From cd945f20f7ccd13109a2a8af29c5813ad40974eb Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Thu, 29 Dec 2022 15:04:24 +0000 Subject: [PATCH 001/218] feat: Added Lightbox --- .../src/model/article-schema.json | 6 + .../src/model/enhanceImagesForLightbox.ts | 35 ++ dotcom-rendering/src/types/frontend.ts | 3 +- .../src/web/components/ArticlePage.tsx | 6 + .../src/web/components/Caption.tsx | 41 ++ .../src/web/components/ImageComponent.tsx | 62 ++- .../src/web/components/Lightbox.stories.tsx | 356 ++++++++++++ .../src/web/components/Lightbox.tsx | 513 ++++++++++++++++++ .../src/web/components/LightboxButton.tsx | 73 +++ .../LightboxJavascript.importable.tsx | 249 +++++++++ dotcom-rendering/src/web/server/index.ts | 5 + 11 files changed, 1323 insertions(+), 26 deletions(-) create mode 100644 dotcom-rendering/src/model/enhanceImagesForLightbox.ts create mode 100644 dotcom-rendering/src/web/components/Lightbox.stories.tsx create mode 100644 dotcom-rendering/src/web/components/Lightbox.tsx create mode 100644 dotcom-rendering/src/web/components/LightboxButton.tsx create mode 100644 dotcom-rendering/src/web/components/LightboxJavascript.importable.tsx diff --git a/dotcom-rendering/src/model/article-schema.json b/dotcom-rendering/src/model/article-schema.json index 4a93b04d020..b321e3bf563 100644 --- a/dotcom-rendering/src/model/article-schema.json +++ b/dotcom-rendering/src/model/article-schema.json @@ -571,6 +571,12 @@ }, "canonicalUrl": { "type": "string" + }, + "imagesForLightbox": { + "type": "array", + "items": { + "$ref": "#/definitions/ImageBlockElement" + } } }, "required": [ diff --git a/dotcom-rendering/src/model/enhanceImagesForLightbox.ts b/dotcom-rendering/src/model/enhanceImagesForLightbox.ts new file mode 100644 index 00000000000..94bc7fbb037 --- /dev/null +++ b/dotcom-rendering/src/model/enhanceImagesForLightbox.ts @@ -0,0 +1,35 @@ +import type { CAPIElement, ImageBlockElement } from '../types/content'; + +export const enhanceImagesForLightbox = ( + blocks: Block[], + mainMediaElements: CAPIElement[], +): ImageBlockElement[] | undefined => { + const images: ImageBlockElement[] = []; + mainMediaElements.forEach((element) => { + if ( + element._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ) { + images.push(element); + } + }); + blocks.forEach((block) => { + block.elements.forEach((element) => { + switch (element._type) { + case 'model.dotcomrendering.pageElements.ImageBlockElement': { + images.push(element); + break; + } + case 'model.dotcomrendering.pageElements.MultiImageBlockElement': { + element.images.forEach((image) => { + images.push(image); + }); + break; + } + default: + break; + } + }); + }); + return images; +}; diff --git a/dotcom-rendering/src/types/frontend.ts b/dotcom-rendering/src/types/frontend.ts index 535a7fcf72c..4aa2ea20c53 100644 --- a/dotcom-rendering/src/types/frontend.ts +++ b/dotcom-rendering/src/types/frontend.ts @@ -2,7 +2,7 @@ import type { EditionId } from '../web/lib/edition'; import type { BadgeType } from './badge'; import type { CommercialProperties } from './commercial'; import type { ConfigType } from './config'; -import { CAPIElement, Newsletter } from './content'; +import type { CAPIElement, ImageBlockElement, Newsletter } from './content'; import type { FooterType } from './footer'; import type { CAPIOnwards } from './onwards'; import type { TagType } from './tag'; @@ -112,6 +112,7 @@ export interface FEArticleType { promotedNewsletter?: Newsletter; tableOfContents?: TableOfContentsItem[]; canonicalUrl: string; + imagesForLightbox?: ImageBlockElement[]; } export interface TableOfContents { diff --git a/dotcom-rendering/src/web/components/ArticlePage.tsx b/dotcom-rendering/src/web/components/ArticlePage.tsx index 4b08b26eb5a..c250e27c1db 100644 --- a/dotcom-rendering/src/web/components/ArticlePage.tsx +++ b/dotcom-rendering/src/web/components/ArticlePage.tsx @@ -13,6 +13,8 @@ import { CoreVitals } from './CoreVitals.importable'; import { FetchCommentCounts } from './FetchCommentCounts.importable'; import { FocusStyles } from './FocusStyles.importable'; import { Island } from './Island'; +import { Lightbox } from './Lightbox'; +import { LightboxJavascript } from './LightboxJavascript.importable'; import { ReaderRevenueDev } from './ReaderRevenueDev.importable'; import { SetABTests } from './SetABTests.importable'; import { SkipTo } from './SkipTo'; @@ -90,6 +92,10 @@ export const ArticlePage = ({ CAPIArticle, NAV, format }: Props) => { isDev={!!CAPIArticle.config.isDev} /> + + + + ); diff --git a/dotcom-rendering/src/web/components/Caption.tsx b/dotcom-rendering/src/web/components/Caption.tsx index 56c03339a90..a85982a73aa 100644 --- a/dotcom-rendering/src/web/components/Caption.tsx +++ b/dotcom-rendering/src/web/components/Caption.tsx @@ -4,6 +4,7 @@ import { ArticleDesign, ArticleDisplay, ArticleSpecial } from '@guardian/libs'; import { between, from, + neutral, space, textSans, until, @@ -22,6 +23,7 @@ type Props = { shouldLimitWidth?: boolean; isOverlaid?: boolean; isLeftCol?: boolean; + isLightbox?: boolean; mediaType?: MediaType; isMainMedia?: boolean; }; @@ -232,6 +234,7 @@ export const Caption = ({ shouldLimitWidth = false, isOverlaid, isLeftCol, + isLightbox, mediaType = 'Gallery', isMainMedia = false, }: Props) => { @@ -277,6 +280,44 @@ export const Caption = ({ ); + if (isLightbox) { + return ( +
+ {!!captionText && ( + + )} + {!!credit && displayCredit && ( +
+ {credit} +
+ )} +
+ ); + } + switch (format.design) { case ArticleDesign.PhotoEssay: if (format.theme === ArticleSpecial.Labs && isLeftCol) { diff --git a/dotcom-rendering/src/web/components/ImageComponent.tsx b/dotcom-rendering/src/web/components/ImageComponent.tsx index f999e9abc85..900a53a9251 100644 --- a/dotcom-rendering/src/web/components/ImageComponent.tsx +++ b/dotcom-rendering/src/web/components/ImageComponent.tsx @@ -13,6 +13,7 @@ import type { Palette } from '../../types/palette'; import { decidePalette } from '../lib/decidePalette'; import { Caption } from './Caption'; import { Hide } from './Hide'; +import { LightboxButton } from './LightboxButton'; import { Picture } from './Picture'; import { StarRating } from './StarRating/StarRating'; @@ -213,6 +214,37 @@ const CaptionToggle = () => ( ); +export const getMaster = (images: Image[]) => { + return images.find((image) => image.fields.isMaster)?.url; +}; +export const getLargest = (images: Image[]) => { + const descendingByWidth = (a: Image, b: Image) => { + return parseInt(b.fields.width) - parseInt(a.fields.width); + }; + return images.slice().sort(descendingByWidth)[0]?.url; +}; + +/** + * We get the first 'media' height and width. This doesn't match the actual image height and width but that's ok + * because the image sources and CSS deal with the sizing. What the height and width gives us is a true + * ratio to apply to the image in the page, so the browser's pre-parser can reserve the space. + * + * The default is the 5:3 standard that The Grid suggests, at our wide breakpoint width. + */ +export const getImageDimensions = ( + element: ImageBlockElement, +): { + width: string; + height: string; +} => { + const width = element.media.allImages[0]?.fields.width ?? '620'; + const height = element.media.allImages[0]?.fields.height ?? '372'; + return { + width, + height, + }; +}; + export const ImageComponent = ({ element, format, @@ -236,34 +268,12 @@ export const ImageComponent = ({ format.design !== ArticleDesign.Comment && format.design !== ArticleDesign.Editorial; - // We get the first 'media' height and width. This doesn't match the actual image height and width but that's ok - // because the image sources and CSS deal with the sizing. What the height and width gives us is a true - // ratio to apply to the image in the page, so the browser's pre-parser can reserve the space. - // - // The default is the 5:3 standard that The Grid suggests, at our wide breakpoint width. - const imageWidth = - (element.media && - element.media.allImages[0] && - element.media.allImages[0].fields.width) || - '620'; - const imageHeight = - (element.media && - element.media.allImages[0] && - element.media.allImages[0].fields.height) || - '372'; + const dimensions = getImageDimensions(element); + const imageWidth = dimensions.width; + const imageHeight = dimensions.height; const palette = decidePalette(format); - const getMaster = (images: Image[]) => { - return images.find((image) => image.fields.isMaster)?.url; - }; - const getLargest = (images: Image[]) => { - const descendingByWidth = (a: Image, b: Image) => { - return parseInt(b.fields.width) - parseInt(a.fields.width); - }; - return images.slice().sort(descendingByWidth)[0]?.url; - }; - // Legacy images do not have a master so we fallback to the largest available const image = getMaster(element.media.allImages) ?? @@ -356,6 +366,7 @@ export const ImageComponent = ({ {!!title && ( )} + ); } @@ -428,6 +439,7 @@ export const ImageComponent = ({ {!!title && ( )} + {isMainMedia ? ( diff --git a/dotcom-rendering/src/web/components/Lightbox.stories.tsx b/dotcom-rendering/src/web/components/Lightbox.stories.tsx new file mode 100644 index 00000000000..c4de24abe40 --- /dev/null +++ b/dotcom-rendering/src/web/components/Lightbox.stories.tsx @@ -0,0 +1,356 @@ +import { + ArticleDesign, + ArticleDisplay, + ArticlePillar, + ArticleSpecial, +} from '@guardian/libs'; +import { breakpoints } from '@guardian/source-foundations'; +import { useEffect } from 'react'; +import type { ImageBlockElement } from '../../types/content'; +import { Lightbox } from './Lightbox'; + +const testImage: ImageBlockElement = { + role: 'immersive', + elementId: 'mockId', + data: { + alt: '26 March – Residents in Manchester, England, taking part in the Clap For Our Carers campaign, people across the UK applauding NHS workers from the doorsteps, balconies or windows of their homes, in order to say thank you for all of their hard work dealing with the Covid-19 coronavirus pandemic.', + caption: + '26 March – Residents in Manchester, England, taking part in the Clap For Our Carers campaign, people across the UK applauding NHS workers from the doorsteps, balconies or windows of their homes, in order to say thank you for all of their hard work dealing with the Covid-19 coronavirus pandemic', + credit: 'Photograph: Christopher Thomond/The Guardian', + }, + imageSources: [ + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/56b42eef576bc04c820da710459acd91082bb37b/0_0_6720_4480/master/6720.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=4a0b1263ca8554c4e22671fd1a75ac64', + width: 2600, + }, + ], + }, + ], + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + media: { + allImages: [ + { + index: 0, + fields: { + height: '4480', + width: '6720', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/56b42eef576bc04c820da710459acd91082bb37b/0_0_6720_4480/6720.jpg', + }, + ], + }, + displayCredit: false, +}; + +export default { + component: Lightbox, + title: 'Components/Lightbox', + parameters: { + chromatic: { + viewports: [breakpoints.mobile, breakpoints.desktop], + }, + viewport: { + // This has the effect of turning off the viewports addon by default + defaultViewport: 'doesNotExist', + }, + }, +}; + +function showModal() { + const lightbox = document.querySelector('#gu-lightbox'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue + (lightbox as any)?.showModal(); // See: https://github.com/microsoft/TypeScript/issues/48267 +} + +function showInfo() { + const lightbox = document.querySelector('#gu-lightbox'); + const imageList = lightbox?.querySelector('ul'); + imageList?.classList.remove('hide-info'); +} + +export const Default = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithTitle = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithCredit = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithRating = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithEverything = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithoutCaption = () => { + useEffect(() => { + showModal(); + }); + return ( + + ); +}; + +export const WithSport = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithCulture = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithLifestyle = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithOpinion = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithSpecialReport = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithSpecialReportAlt = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +export const WithLabs = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + +/** + * with html in caption (links) + * with a portrait image + * + */ diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx new file mode 100644 index 00000000000..44133ba3664 --- /dev/null +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -0,0 +1,513 @@ +import { css, Global } from '@emotion/react'; +import { + brandAltBackground, + from, + headline, + neutral, + space, + textSans, + until, + visuallyHidden, +} from '@guardian/source-foundations'; +import { + Hide, + SvgArrowLeftStraight, + SvgArrowRightStraight, + SvgCross, +} from '@guardian/source-react-components'; +import { StarRating } from '@guardian/source-react-components-development-kitchen'; +import type { ImageBlockElement } from '../../types/content'; +import { Caption } from './Caption'; +import { getImageDimensions, getLargest, getMaster } from './ImageComponent'; +import { Picture } from './Picture'; + +type Props = { + format: ArticleFormat; + images?: ImageBlockElement[]; +}; + +const dialogStyles = css` + width: 100vw; + height: 100vh; + border: none; + max-width: 100vw; + max-height: 100vh; + padding: 0; + background-color: ${neutral[10]}; + ::backdrop { + background-color: ${neutral[10]}; + } + + ul.hide-info aside { + display: none; + } + + button.selected { + background-color: ${neutral[46]}; + } +`; + +const containerStyles = css` + display: flex; + height: 100%; + flex-direction: row-reverse; + ${until.tablet} { + flex-direction: column; + } +`; + +const navStyles = css` + display: flex; + flex-direction: column; + ${until.tablet} { + flex-direction: row; + } + ${from.tablet} { + padding-top: ${space[3]}px; + height: 100vh; + } + color: white; + ${until.tablet} { + border-bottom: 0.0625rem solid ${neutral[20]}; + } + ${from.tablet} { + border-left: 0.0625rem solid ${neutral[20]}; + } + background-color: ${neutral[7]}; +`; + +const ulStyles = css` + display: flex; + height: 100%; + scroll-snap-type: x mandatory; + overflow-x: scroll; + scroll-behavior: auto; + ${from.tablet} { + margin-left: ${space[5]}px; + } +`; + +const liStyles = css` + width: 100%; + flex-shrink: 0; + scroll-snap-align: start; +`; + +const imageStyles = (orientation: 'horizontal' | 'portrait') => { + switch (orientation) { + case 'portrait': { + return css` + img { + object-fit: contain; + object-position: top; + width: auto; + max-width: 100%; + margin-left: auto; + margin-right: auto; + margin-top: ${space[3]}px; + ${from.tablet} { + margin-top: ${space[5]}px; + } + ${until.tablet} { + height: calc(100vh - 90px); + } + ${from.tablet} { + height: calc(100vh - 24px); + } + } + picture { + flex-grow: 1; + ${from.tablet} { + margin-right: ${space[5]}px; + } + } + `; + } + case 'horizontal': + default: { + return css` + img { + object-fit: contain; + width: 100%; + height: auto; + margin-left: auto; + margin-right: auto; + margin-top: ${space[3]}px; + ${from.tablet} { + margin-top: ${space[5]}px; + } + ${until.tablet} { + max-height: calc(100vh - 90px); + } + ${from.tablet} { + max-height: calc(100vh - 24px); + } + } + picture { + flex-grow: 1; + ${from.tablet} { + margin-right: ${space[5]}px; + } + } + `; + } + } +}; + +const asideStyles = css` + ${until.tablet} { + width: 100%; + margin-top: ${space[3]}px; + position: absolute; + bottom: 0; + left: 0; + } + ${from.tablet} { + min-width: 300px; + max-width: 300px; + } + ${until.tablet} { + border-top: 0.0625rem solid ${neutral[20]}; + } + ${from.tablet} { + border-left: 0.0625rem solid ${neutral[20]}; + } + background-color: ${neutral[7]}; + + padding-left: ${space[3]}px; + padding-right: ${space[3]}px; + padding-top: ${space[3]}px; + padding-bottom: ${space[3]}px; + + ${from.tablet} { + padding-top: ${space[5]}px; + padding-left: ${space[5]}px; + padding-right: ${space[5]}px; + } +`; + +const figureStyles = css` + display: flex; + position: relative; + height: 100%; + justify-content: space-between; + + ${until.tablet} { + flex-direction: column; + } + ${from.tablet} { + flex-direction: row; + } +`; + +const buttonStyles = css` + svg { + fill: ${neutral[100]}; + } + margin: ${space[2]}px; + border-radius: 50%; + height: 44px; + width: 44px; + border: none; + cursor: pointer; + background-color: ${neutral[20]}; + :hover { + background-color: ${neutral[46]}; + } +`; + +const closeButtonStyles = css` + ${until.tablet} { + position: absolute; + right: ${space[2]}px; + } + svg { + height: 24px; + width: 24px; + } +`; + +const arrowButtonStyles = css` + svg { + height: 32px; + width: 32px; + } +`; + +const infoButtonStyles = css` + svg { + height: 24px; + width: 24px; + } +`; + +const lowlightStyles = css` + ${textSans.xsmall()}; + color: ${neutral[86]}; +`; + +const highlightedStyles = css` + ${textSans.xsmall()}; + color: ${neutral[97]}; +`; + +const SvgInfo = () => ( + + + +); + +const Selection = ({ + countOfImages, + initialPosition = 1, +}: { + countOfImages: number; + initialPosition?: number; +}) => { + return ( + + + {initialPosition} + + + / + + {countOfImages} + + ); +}; + +export const Lightbox = ({ format, images }: Props) => { + if (!images?.length) return null; + + return ( + <> + + +
+ +
    + {images.map((image, index) => { + // Legacy images do not have a master so we fallback to the largest available + const master: string | undefined = + getMaster(image.media.allImages) ?? + getLargest(image.media.allImages); + + if (!master) return null; + + const dimensions = getImageDimensions(image); + const width = dimensions.width; + const height = dimensions.height; + + const orientation = + parseInt(width) > parseInt(height) + ? 'horizontal' + : 'portrait'; + + return ( +
  • +
    + + +
    +
  • + ); + })} +
+
+
+ + ); +}; diff --git a/dotcom-rendering/src/web/components/LightboxButton.tsx b/dotcom-rendering/src/web/components/LightboxButton.tsx new file mode 100644 index 00000000000..ca327ac7b3f --- /dev/null +++ b/dotcom-rendering/src/web/components/LightboxButton.tsx @@ -0,0 +1,73 @@ +import { css } from '@emotion/react'; +import { neutral, space, visuallyHidden } from '@guardian/source-foundations'; +import { SvgArrowExpand } from '@guardian/source-react-components'; +import type { RoleType } from '../../types/content'; + +type Props = { + elementId: string; + role: RoleType; +}; + +function decideSize(role: RoleType) { + switch (role) { + case 'halfWidth': + case 'supporting': { + return css` + height: 30px; + width: 30px; + `; + } + case 'inline': + case 'showcase': + case 'immersive': + default: { + return css` + height: 44px; + width: 44px; + `; + } + } +} + +export const LightboxButton = ({ elementId, role }: Props) => { + // Don't show the button over thumbnails; they're too small + if (role === 'thumbnail') return null; + return ( + + ); +}; diff --git a/dotcom-rendering/src/web/components/LightboxJavascript.importable.tsx b/dotcom-rendering/src/web/components/LightboxJavascript.importable.tsx new file mode 100644 index 00000000000..4ae22b581f2 --- /dev/null +++ b/dotcom-rendering/src/web/components/LightboxJavascript.importable.tsx @@ -0,0 +1,249 @@ +import libDebounce from 'lodash.debounce'; +import { useEffect } from 'react'; + +export const LightboxJavascript = () => { + useEffect(() => { + // Document selectors + const lightbox = + document.querySelector('#gu-lightbox'); + const lightboxButtons = document.querySelectorAll( + 'button.open-lightbox', + ); + + // Lightbox selectors + const nextButton = + lightbox?.querySelector('button.next'); + const previousButton = + lightbox?.querySelector('button.previous'); + const infoButton = + lightbox?.querySelector('button.info'); + const closeButton = + lightbox?.querySelector('button.close'); + const positionDisplay = + lightbox?.querySelector('.selected'); + const imageList = lightbox?.querySelector('ul'); + const totalNoOfImages = + lightbox?.querySelectorAll('li img').length ?? 1; + + // Functions + function select(position: number): void { + if (positionDisplay) { + positionDisplay.innerHTML = position.toString(); + } + } + + function pulseButton(button: HTMLButtonElement): void { + button.classList.add('selected'); + + window.setTimeout(() => { + button.classList.remove('selected'); + }, 75); + } + + function scrollTo(position: number): void { + const liWidth = lightbox?.querySelector('li')?.clientWidth; + if (!imageList || !liWidth) return; + switch (position) { + case 0: + case 1: { + imageList.scrollLeft = 0; + break; + } + default: { + imageList.scrollLeft = (position - 1) * liWidth; + } + } + } + + function getPosition(): number { + const scrollPosition = imageList?.scrollLeft; + const liWidth = lightbox?.querySelector('li')?.clientWidth; + if (liWidth && scrollPosition) { + return Math.round(scrollPosition / liWidth) + 1; + } + return 1; + } + + function getPreviousPosition(positionNow: number): number { + if (positionNow === 1) { + // Cycle around to the end + return totalNoOfImages; + } else { + return positionNow - 1; + } + } + + function getNextPosition(positionNow: number): number { + if (positionNow === totalNoOfImages) { + // Cycle back to the start + return 1; + } else { + return positionNow + 1; + } + } + + function loadAdjacentImages(currentPosition: number): void { + function eagerLoad(position: number) { + const allImages = + lightbox?.querySelectorAll('li img'); + const imgArray = allImages ? Array.from(allImages) : []; + const imgElement = imgArray[position - 1]; + if (imgElement) imgElement.loading = 'eager'; + } + const previousImage = getPreviousPosition(currentPosition); + const nextImage = getNextPosition(currentPosition); + eagerLoad(previousImage); + eagerLoad(nextImage); + } + + function goBack(): void { + if (previousButton) pulseButton(previousButton); + const positionNow = getPosition(); + const newPosition = getPreviousPosition(positionNow); + select(newPosition); + scrollTo(newPosition); + loadAdjacentImages(newPosition); + } + + function goForward(): void { + if (nextButton) pulseButton(nextButton); + const positionNow = getPosition(); + const newPosition = getNextPosition(positionNow); + select(newPosition); + scrollTo(newPosition); + loadAdjacentImages(newPosition); + } + + function close(): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue + (lightbox as any)?.close(); // See: https://github.com/microsoft/TypeScript/issues/48267 + } + + function showInfo(): void { + infoButton?.classList.add('selected'); + imageList?.classList.remove('hide-info'); + } + + function hideInfo(): void { + infoButton?.classList.remove('selected'); + imageList?.classList.add('hide-info'); + } + + function toggleInfo(): void { + infoButton?.classList.toggle('selected'); + imageList?.classList.toggle('hide-info'); + } + + // Event listeners + lightboxButtons.forEach((button) => { + button.addEventListener('click', () => { + // We use this class to prevent the main page from scrolling + document.documentElement.classList.add('lightbox-open'); + // Extract the element id for this image out of the button that was clicked on + const elementId = button.dataset.elementId; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue + (lightbox as any)?.showModal(); // See: https://github.com/microsoft/TypeScript/issues/48267 + // Find the image inside the lightbox and get its index + const imageWrapper: HTMLLIElement | null | undefined = elementId + ? lightbox?.querySelector( + `li[data-element-id="${elementId}"]`, + ) + : null; + const stringIndex: string = imageWrapper?.dataset.index ?? '1'; + const indexOfImageClicked = parseInt(stringIndex); + // Now we have the index of the image that was clicked, show it + // in the lightbox + select(indexOfImageClicked); + scrollTo(indexOfImageClicked); + loadAdjacentImages(indexOfImageClicked); + }); + }); + + lightbox?.addEventListener('keydown', (event) => { + switch (event.code) { + case 'Tab': { + // We're completely taking over tabbing to keep focus inside + // the dialog + event.preventDefault(); + switch (document.activeElement) { + case closeButton: + event.shiftKey + ? infoButton?.focus() + : previousButton?.focus(); + break; + case previousButton: + event.shiftKey + ? closeButton?.focus() + : nextButton?.focus(); + break; + case nextButton: + event.shiftKey + ? previousButton?.focus() + : infoButton?.focus(); + break; + case infoButton: + event.shiftKey + ? nextButton?.focus() + : closeButton?.focus(); + break; + default: + break; + } + break; + } + case 'ArrowLeft': + goBack(); + break; + case 'ArrowRight': + goForward(); + break; + case 'KeyI': + toggleInfo(); + break; + case 'KeyQ': + close(); + break; + case 'ArrowUp': + showInfo(); + break; + case 'ArrowDown': + hideInfo(); + break; + } + }); + + imageList?.addEventListener( + 'scroll', + libDebounce(() => { + const currentPosition = getPosition(); + select(currentPosition); + loadAdjacentImages(currentPosition); + }, 300), + ); + + closeButton?.addEventListener('click', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue + (lightbox as any)?.close(); // See: https://github.com/microsoft/TypeScript/issues/48267 + }); + + previousButton?.addEventListener('click', () => { + goBack(); + }); + + nextButton?.addEventListener('click', () => { + goForward(); + }); + + infoButton?.addEventListener('click', () => { + toggleInfo(); + }); + + lightbox?.addEventListener('close', () => { + document.documentElement.classList.remove('lightbox-open'); + }); + }, []); + + // Don’t render anything + return null; +}; diff --git a/dotcom-rendering/src/web/server/index.ts b/dotcom-rendering/src/web/server/index.ts index caaca88b4c6..22c6a8cf412 100644 --- a/dotcom-rendering/src/web/server/index.ts +++ b/dotcom-rendering/src/web/server/index.ts @@ -4,6 +4,7 @@ import { isRecipe } from '../../model/enhance-recipes'; import { enhanceBlocks } from '../../model/enhanceBlocks'; import { enhanceCollections } from '../../model/enhanceCollections'; import { enhanceCommercialProperties } from '../../model/enhanceCommercialProperties'; +import { enhanceImagesForLightbox } from '../../model/enhanceImagesForLightbox'; import { enhanceStandfirst } from '../../model/enhanceStandfirst'; import { enhanceTableOfContents } from '../../model/enhanceTableOfContents'; import { validateAsCAPIType, validateAsFrontType } from '../../model/validate'; @@ -38,6 +39,10 @@ const enhanceCAPIType = (body: unknown): FEArticleType => { tableOfContents: data.config.switches.tableOfContents ? enhanceTableOfContents(data.format, enhancedBlocks) : undefined, + imagesForLightbox: enhanceImagesForLightbox( + enhancedBlocks, + data.mainMediaElements, + ), }; return CAPIArticle; }; From 39c099ad85bcaf541f66d402dc33efb35cad070f Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Thu, 29 Dec 2022 15:30:07 +0000 Subject: [PATCH 002/218] feat: Use theme based colours for the divider --- dotcom-rendering/src/types/palette.ts | 1 + dotcom-rendering/src/web/components/Caption.tsx | 2 +- dotcom-rendering/src/web/lib/decidePalette.ts | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/types/palette.ts b/dotcom-rendering/src/types/palette.ts index e98c063d15b..c0b50539263 100644 --- a/dotcom-rendering/src/types/palette.ts +++ b/dotcom-rendering/src/types/palette.ts @@ -94,6 +94,7 @@ export type Palette = { treat: Colour; designTag: Colour; pullQuote: Colour; + lightboxDivider: Colour; }; fill: { commentCount: Colour; diff --git a/dotcom-rendering/src/web/components/Caption.tsx b/dotcom-rendering/src/web/components/Caption.tsx index a85982a73aa..aac1fe7d394 100644 --- a/dotcom-rendering/src/web/components/Caption.tsx +++ b/dotcom-rendering/src/web/components/Caption.tsx @@ -288,7 +288,7 @@ export const Caption = ({ line-height: 135%; word-wrap: break4all; padding-top: ${space[2]}px; - border-top: 3px solid ${palette.text.caption}; + border-top: 3px solid ${palette.background.lightboxDivider}; `} > {!!captionText && ( diff --git a/dotcom-rendering/src/web/lib/decidePalette.ts b/dotcom-rendering/src/web/lib/decidePalette.ts index c2050f59940..b965f7e671f 100644 --- a/dotcom-rendering/src/web/lib/decidePalette.ts +++ b/dotcom-rendering/src/web/lib/decidePalette.ts @@ -1027,6 +1027,8 @@ const backgroundImageTitle = (format: ArticleFormat): string => { return pillarPalette[format.theme].main; }; +const backgroundLightboxDivider = backgroundImageTitle; + const backgroundSpeechBubble = (format: ArticleFormat): string => { if (format.design === ArticleDesign.Analysis) { switch (format.theme) { @@ -2105,6 +2107,7 @@ export const decidePalette = ( standfirst: backgroundStandfirst(format), richLink: backgroundRichLink(format), imageTitle: backgroundImageTitle(format), + lightboxDivider: backgroundLightboxDivider(format), speechBubble: backgroundSpeechBubble(format), carouselDot: backgroundCarouselDot(format), carouselDotFocus: backgroundCarouselDotFocus(format), From 83ac89d716cca871ba63655917babbfad131eb60 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Fri, 30 Dec 2022 10:30:36 +0000 Subject: [PATCH 003/218] feat: Only trigger simulated click if hydration succeeded Sometimes the `doHydration` function will abort - e.g.: if it has already been run - and in these circumstances we do not want to dispatch the fake mouse event because the real one will work by itself --- .../src/web/browser/islands/doHydration.ts | 6 ++++-- .../src/web/browser/islands/initHydration.ts | 14 +++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/dotcom-rendering/src/web/browser/islands/doHydration.ts b/dotcom-rendering/src/web/browser/islands/doHydration.ts index c773578fbf2..a95c264766b 100644 --- a/dotcom-rendering/src/web/browser/islands/doHydration.ts +++ b/dotcom-rendering/src/web/browser/islands/doHydration.ts @@ -18,11 +18,11 @@ export const doHydration = async ( name: string, data: { [key: string]: unknown } | null, element: HTMLElement, -): Promise => { +): Promise<{ success: boolean }> => { // If this function has already been run for an element then don't try to // run it a second time const alreadyHydrated = element.dataset.guReady; - if (alreadyHydrated) return; + if (alreadyHydrated) return { success: false }; const { start: importStart, end: importEnd } = initPerf(`import-${name}`); importStart(); @@ -72,4 +72,6 @@ export const doHydration = async ( } throw error; }); + + return { success: true }; }; diff --git a/dotcom-rendering/src/web/browser/islands/initHydration.ts b/dotcom-rendering/src/web/browser/islands/initHydration.ts index 2b20333d898..f3b9989ae5a 100644 --- a/dotcom-rendering/src/web/browser/islands/initHydration.ts +++ b/dotcom-rendering/src/web/browser/islands/initHydration.ts @@ -40,11 +40,15 @@ export const initHydration = (elements: NodeListOf): void => { } case 'interaction': { onInteraction(element, (targetElement) => { - void doHydration(name, props, element).then(() => { - targetElement.dispatchEvent( - new MouseEvent('click'), - ); - }); + void doHydration(name, props, element).then( + ({ success }) => { + if (success) { + targetElement.dispatchEvent( + new MouseEvent('click'), + ); + } + }, + ); }); break; } From 8272c6ec479a9c1a56ff6597b936fb71f836caa8 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Fri, 30 Dec 2022 10:32:44 +0000 Subject: [PATCH 004/218] feat: Prevent double hydration where there are duplicate islands on the page This code assumes that there is not a use case where you would want to execute the same island multiple times in different contexts. If there were, you could use a prop like { hydrateOnce: boolean } to control this --- .../src/web/browser/islands/doHydration.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/web/browser/islands/doHydration.ts b/dotcom-rendering/src/web/browser/islands/doHydration.ts index a95c264766b..b6f5a03c5fa 100644 --- a/dotcom-rendering/src/web/browser/islands/doHydration.ts +++ b/dotcom-rendering/src/web/browser/islands/doHydration.ts @@ -19,9 +19,15 @@ export const doHydration = async ( data: { [key: string]: unknown } | null, element: HTMLElement, ): Promise<{ success: boolean }> => { - // If this function has already been run for an element then don't try to - // run it a second time - const alreadyHydrated = element.dataset.guReady; + // If this function has already been run for this element or any other island + // with the same name then don't try to run it a second time + let alreadyHydrated = false; + document + .querySelectorAll(`gu-island[name="${name}"]`) + .forEach((island) => { + if (island.dataset.guReady) alreadyHydrated = true; + }); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- because it is necessary if (alreadyHydrated) return { success: false }; const { start: importStart, end: importEnd } = initPerf(`import-${name}`); From d32fc690519b99920ff1e71e80324b30cfa45f98 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Fri, 30 Dec 2022 10:34:27 +0000 Subject: [PATCH 005/218] feat: Handle when the click event lands on an svg inside a button --- dotcom-rendering/src/web/browser/islands/initHydration.ts | 4 +++- dotcom-rendering/src/web/browser/islands/onInteraction.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/web/browser/islands/initHydration.ts b/dotcom-rendering/src/web/browser/islands/initHydration.ts index f3b9989ae5a..9a5593fe942 100644 --- a/dotcom-rendering/src/web/browser/islands/initHydration.ts +++ b/dotcom-rendering/src/web/browser/islands/initHydration.ts @@ -44,7 +44,9 @@ export const initHydration = (elements: NodeListOf): void => { ({ success }) => { if (success) { targetElement.dispatchEvent( - new MouseEvent('click'), + new MouseEvent('click', { + bubbles: true, + }), ); } }, diff --git a/dotcom-rendering/src/web/browser/islands/onInteraction.ts b/dotcom-rendering/src/web/browser/islands/onInteraction.ts index d06bc73fd8c..f1c2078e4a1 100644 --- a/dotcom-rendering/src/web/browser/islands/onInteraction.ts +++ b/dotcom-rendering/src/web/browser/islands/onInteraction.ts @@ -7,12 +7,12 @@ */ export const onInteraction = ( element: HTMLElement, - callback: (e: HTMLElement) => void, + callback: (target: Element) => void, ): void => { element.addEventListener( 'click', (e) => { - if (e.target instanceof HTMLElement) { + if (e.target instanceof Element) { callback(e.target); } }, From 3597edc2de700e35d6680e014f8dff3ff5f0184d Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Fri, 30 Dec 2022 10:40:44 +0000 Subject: [PATCH 006/218] hack: Wait for the island javascript to execute This is clearly a sub optimal solution but it provides a solution to the race condition where the island code sets the click event listener which needs to exist prior to any simulated click being fired. 50ms won't be noticed by readers but it is a long time to wait for code to execute so this solution is likely to be sufficient in most cases. Where it isn't a second click would be needed. --- .../src/web/browser/islands/initHydration.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/web/browser/islands/initHydration.ts b/dotcom-rendering/src/web/browser/islands/initHydration.ts index 9a5593fe942..d31cc1766d6 100644 --- a/dotcom-rendering/src/web/browser/islands/initHydration.ts +++ b/dotcom-rendering/src/web/browser/islands/initHydration.ts @@ -43,11 +43,13 @@ export const initHydration = (elements: NodeListOf): void => { void doHydration(name, props, element).then( ({ success }) => { if (success) { - targetElement.dispatchEvent( - new MouseEvent('click', { - bubbles: true, - }), - ); + setTimeout(() => { + targetElement.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + }), + ); + }, 50); } }, ); From 8526ae60b0e98786fe3d9249dd5076ff02e87872 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Fri, 30 Dec 2022 10:43:16 +0000 Subject: [PATCH 007/218] feat: Defer hydration until interaction --- .../src/web/components/ArticlePage.tsx | 6 +- .../src/web/components/ImageComponent.tsx | 11 +- .../components/LightboxButton.importable.tsx | 314 ++++++++++++++++++ .../src/web/components/LightboxButton.tsx | 73 ---- .../LightboxJavascript.importable.tsx | 249 -------------- 5 files changed, 323 insertions(+), 330 deletions(-) create mode 100644 dotcom-rendering/src/web/components/LightboxButton.importable.tsx delete mode 100644 dotcom-rendering/src/web/components/LightboxButton.tsx delete mode 100644 dotcom-rendering/src/web/components/LightboxJavascript.importable.tsx diff --git a/dotcom-rendering/src/web/components/ArticlePage.tsx b/dotcom-rendering/src/web/components/ArticlePage.tsx index c250e27c1db..53507288fc8 100644 --- a/dotcom-rendering/src/web/components/ArticlePage.tsx +++ b/dotcom-rendering/src/web/components/ArticlePage.tsx @@ -14,7 +14,6 @@ import { FetchCommentCounts } from './FetchCommentCounts.importable'; import { FocusStyles } from './FocusStyles.importable'; import { Island } from './Island'; import { Lightbox } from './Lightbox'; -import { LightboxJavascript } from './LightboxJavascript.importable'; import { ReaderRevenueDev } from './ReaderRevenueDev.importable'; import { SetABTests } from './SetABTests.importable'; import { SkipTo } from './SkipTo'; @@ -52,6 +51,7 @@ export const ArticlePage = ({ CAPIArticle, NAV, format }: Props) => { /> + {(format.design === ArticleDesign.LiveBlog || format.design === ArticleDesign.DeadBlog) && ( @@ -92,10 +92,6 @@ export const ArticlePage = ({ CAPIArticle, NAV, format }: Props) => { isDev={!!CAPIArticle.config.isDev} /> - - - - ); diff --git a/dotcom-rendering/src/web/components/ImageComponent.tsx b/dotcom-rendering/src/web/components/ImageComponent.tsx index 900a53a9251..db8f920f6ba 100644 --- a/dotcom-rendering/src/web/components/ImageComponent.tsx +++ b/dotcom-rendering/src/web/components/ImageComponent.tsx @@ -13,7 +13,8 @@ import type { Palette } from '../../types/palette'; import { decidePalette } from '../lib/decidePalette'; import { Caption } from './Caption'; import { Hide } from './Hide'; -import { LightboxButton } from './LightboxButton'; +import { Island } from './Island'; +import { LightboxButton } from './LightboxButton.importable'; import { Picture } from './Picture'; import { StarRating } from './StarRating/StarRating'; @@ -366,7 +367,9 @@ export const ImageComponent = ({ {!!title && ( )} - + + + ); } @@ -439,7 +442,9 @@ export const ImageComponent = ({ {!!title && ( )} - + + + {isMainMedia ? ( diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx new file mode 100644 index 00000000000..c0ec98c6d9d --- /dev/null +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -0,0 +1,314 @@ +import { css } from '@emotion/react'; +import { neutral, space, visuallyHidden } from '@guardian/source-foundations'; +import { SvgArrowExpand } from '@guardian/source-react-components'; +import libDebounce from 'lodash.debounce'; +import { useEffect } from 'react'; +import type { RoleType } from '../../types/content'; + +type Props = { + elementId: string; + role: RoleType; +}; + +function decideSize(role: RoleType) { + switch (role) { + case 'halfWidth': + case 'supporting': { + return css` + height: 30px; + width: 30px; + `; + } + case 'inline': + case 'showcase': + case 'immersive': + default: { + return css` + height: 44px; + width: 44px; + `; + } + } +} + +function initialiseLightbox(lightbox: HTMLDialogElement) { + // Document selectors + const lightboxButtons = document.querySelectorAll( + 'button.open-lightbox', + ); + + // Lightbox selectors + const nextButton = lightbox.querySelector('button.next'); + const previousButton = + lightbox.querySelector('button.previous'); + const infoButton = lightbox.querySelector('button.info'); + const closeButton = + lightbox.querySelector('button.close'); + const positionDisplay = lightbox.querySelector('.selected'); + const imageList = lightbox.querySelector('ul'); + const totalNoOfImages = lightbox.querySelectorAll('li img').length ?? 1; + + // Functions + function select(position: number): void { + if (positionDisplay) { + positionDisplay.innerHTML = position.toString(); + } + } + + function pulseButton(button: HTMLButtonElement): void { + button.classList.add('selected'); + + window.setTimeout(() => { + button.classList.remove('selected'); + }, 75); + } + + function scrollTo(position: number): void { + const liWidth = lightbox.querySelector('li')?.clientWidth; + if (!imageList || !liWidth) return; + switch (position) { + case 0: + case 1: { + imageList.scrollLeft = 0; + break; + } + default: { + imageList.scrollLeft = (position - 1) * liWidth; + } + } + } + + function getPosition(): number { + const scrollPosition = imageList?.scrollLeft; + const liWidth = lightbox.querySelector('li')?.clientWidth; + if (liWidth && scrollPosition) { + return Math.round(scrollPosition / liWidth) + 1; + } + return 1; + } + + function getPreviousPosition(positionNow: number): number { + if (positionNow === 1) { + // Cycle around to the end + return totalNoOfImages; + } else { + return positionNow - 1; + } + } + + function getNextPosition(positionNow: number): number { + if (positionNow === totalNoOfImages) { + // Cycle back to the start + return 1; + } else { + return positionNow + 1; + } + } + + function loadAdjacentImages(currentPosition: number): void { + function eagerLoad(position: number) { + const allImages = + lightbox.querySelectorAll('li img'); + const imgArray = allImages ? Array.from(allImages) : []; + const imgElement = imgArray[position - 1]; + if (imgElement) imgElement.loading = 'eager'; + } + const previousImage = getPreviousPosition(currentPosition); + const nextImage = getNextPosition(currentPosition); + eagerLoad(previousImage); + eagerLoad(nextImage); + } + + function goBack(): void { + if (previousButton) pulseButton(previousButton); + const positionNow = getPosition(); + const newPosition = getPreviousPosition(positionNow); + select(newPosition); + scrollTo(newPosition); + loadAdjacentImages(newPosition); + } + + function goForward(): void { + if (nextButton) pulseButton(nextButton); + const positionNow = getPosition(); + const newPosition = getNextPosition(positionNow); + select(newPosition); + scrollTo(newPosition); + loadAdjacentImages(newPosition); + } + + function close(): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue + (lightbox as any)?.close(); // See: https://github.com/microsoft/TypeScript/issues/48267 + } + + function showInfo(): void { + infoButton?.classList.add('selected'); + imageList?.classList.remove('hide-info'); + } + + function hideInfo(): void { + infoButton?.classList.remove('selected'); + imageList?.classList.add('hide-info'); + } + + function toggleInfo(): void { + infoButton?.classList.toggle('selected'); + imageList?.classList.toggle('hide-info'); + } + + // Event listeners + lightboxButtons.forEach((button) => { + button.addEventListener('click', () => { + // We use this class to prevent the main page from scrolling + document.documentElement.classList.add('lightbox-open'); + // Extract the element id for this image out of the button that was clicked on + const elementId = button.dataset.elementId; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue + (lightbox as any)?.showModal(); // See: https://github.com/microsoft/TypeScript/issues/48267 + // Find the image inside the lightbox and get its index + const imageWrapper: HTMLLIElement | null = elementId + ? lightbox.querySelector(`li[data-element-id="${elementId}"]`) + : null; + const stringIndex: string = imageWrapper?.dataset.index ?? '1'; + const indexOfImageClicked = parseInt(stringIndex); + // Now we have the index of the image that was clicked, show it + // in the lightbox + select(indexOfImageClicked); + scrollTo(indexOfImageClicked); + loadAdjacentImages(indexOfImageClicked); + }); + }); + + lightbox.addEventListener('keydown', (event) => { + switch (event.code) { + case 'Tab': { + // We're completely taking over tabbing to keep focus inside + // the dialog + event.preventDefault(); + switch (document.activeElement) { + case closeButton: + event.shiftKey + ? infoButton?.focus() + : previousButton?.focus(); + break; + case previousButton: + event.shiftKey + ? closeButton?.focus() + : nextButton?.focus(); + break; + case nextButton: + event.shiftKey + ? previousButton?.focus() + : infoButton?.focus(); + break; + case infoButton: + event.shiftKey + ? nextButton?.focus() + : closeButton?.focus(); + break; + default: + break; + } + break; + } + case 'ArrowLeft': + goBack(); + break; + case 'ArrowRight': + goForward(); + break; + case 'KeyI': + toggleInfo(); + break; + case 'KeyQ': + close(); + break; + case 'ArrowUp': + showInfo(); + break; + case 'ArrowDown': + hideInfo(); + break; + } + }); + + imageList?.addEventListener( + 'scroll', + libDebounce(() => { + const currentPosition = getPosition(); + select(currentPosition); + loadAdjacentImages(currentPosition); + }, 300), + ); + + closeButton?.addEventListener('click', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue + (lightbox as any)?.close(); // See: https://github.com/microsoft/TypeScript/issues/48267 + }); + + previousButton?.addEventListener('click', () => { + goBack(); + }); + + nextButton?.addEventListener('click', () => { + goForward(); + }); + + infoButton?.addEventListener('click', () => { + toggleInfo(); + }); + + lightbox.addEventListener('close', () => { + document.documentElement.classList.remove('lightbox-open'); + }); +} + +export const LightboxButton = ({ elementId, role }: Props) => { + useEffect(() => { + const lightbox = + document.querySelector('#gu-lightbox'); + if (lightbox) initialiseLightbox(lightbox); + }, []); + + // Don't show the button over thumbnails; they're too small + if (role === 'thumbnail') return null; + return ( + + ); +}; diff --git a/dotcom-rendering/src/web/components/LightboxButton.tsx b/dotcom-rendering/src/web/components/LightboxButton.tsx deleted file mode 100644 index ca327ac7b3f..00000000000 --- a/dotcom-rendering/src/web/components/LightboxButton.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { css } from '@emotion/react'; -import { neutral, space, visuallyHidden } from '@guardian/source-foundations'; -import { SvgArrowExpand } from '@guardian/source-react-components'; -import type { RoleType } from '../../types/content'; - -type Props = { - elementId: string; - role: RoleType; -}; - -function decideSize(role: RoleType) { - switch (role) { - case 'halfWidth': - case 'supporting': { - return css` - height: 30px; - width: 30px; - `; - } - case 'inline': - case 'showcase': - case 'immersive': - default: { - return css` - height: 44px; - width: 44px; - `; - } - } -} - -export const LightboxButton = ({ elementId, role }: Props) => { - // Don't show the button over thumbnails; they're too small - if (role === 'thumbnail') return null; - return ( - - ); -}; diff --git a/dotcom-rendering/src/web/components/LightboxJavascript.importable.tsx b/dotcom-rendering/src/web/components/LightboxJavascript.importable.tsx deleted file mode 100644 index 4ae22b581f2..00000000000 --- a/dotcom-rendering/src/web/components/LightboxJavascript.importable.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import libDebounce from 'lodash.debounce'; -import { useEffect } from 'react'; - -export const LightboxJavascript = () => { - useEffect(() => { - // Document selectors - const lightbox = - document.querySelector('#gu-lightbox'); - const lightboxButtons = document.querySelectorAll( - 'button.open-lightbox', - ); - - // Lightbox selectors - const nextButton = - lightbox?.querySelector('button.next'); - const previousButton = - lightbox?.querySelector('button.previous'); - const infoButton = - lightbox?.querySelector('button.info'); - const closeButton = - lightbox?.querySelector('button.close'); - const positionDisplay = - lightbox?.querySelector('.selected'); - const imageList = lightbox?.querySelector('ul'); - const totalNoOfImages = - lightbox?.querySelectorAll('li img').length ?? 1; - - // Functions - function select(position: number): void { - if (positionDisplay) { - positionDisplay.innerHTML = position.toString(); - } - } - - function pulseButton(button: HTMLButtonElement): void { - button.classList.add('selected'); - - window.setTimeout(() => { - button.classList.remove('selected'); - }, 75); - } - - function scrollTo(position: number): void { - const liWidth = lightbox?.querySelector('li')?.clientWidth; - if (!imageList || !liWidth) return; - switch (position) { - case 0: - case 1: { - imageList.scrollLeft = 0; - break; - } - default: { - imageList.scrollLeft = (position - 1) * liWidth; - } - } - } - - function getPosition(): number { - const scrollPosition = imageList?.scrollLeft; - const liWidth = lightbox?.querySelector('li')?.clientWidth; - if (liWidth && scrollPosition) { - return Math.round(scrollPosition / liWidth) + 1; - } - return 1; - } - - function getPreviousPosition(positionNow: number): number { - if (positionNow === 1) { - // Cycle around to the end - return totalNoOfImages; - } else { - return positionNow - 1; - } - } - - function getNextPosition(positionNow: number): number { - if (positionNow === totalNoOfImages) { - // Cycle back to the start - return 1; - } else { - return positionNow + 1; - } - } - - function loadAdjacentImages(currentPosition: number): void { - function eagerLoad(position: number) { - const allImages = - lightbox?.querySelectorAll('li img'); - const imgArray = allImages ? Array.from(allImages) : []; - const imgElement = imgArray[position - 1]; - if (imgElement) imgElement.loading = 'eager'; - } - const previousImage = getPreviousPosition(currentPosition); - const nextImage = getNextPosition(currentPosition); - eagerLoad(previousImage); - eagerLoad(nextImage); - } - - function goBack(): void { - if (previousButton) pulseButton(previousButton); - const positionNow = getPosition(); - const newPosition = getPreviousPosition(positionNow); - select(newPosition); - scrollTo(newPosition); - loadAdjacentImages(newPosition); - } - - function goForward(): void { - if (nextButton) pulseButton(nextButton); - const positionNow = getPosition(); - const newPosition = getNextPosition(positionNow); - select(newPosition); - scrollTo(newPosition); - loadAdjacentImages(newPosition); - } - - function close(): void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue - (lightbox as any)?.close(); // See: https://github.com/microsoft/TypeScript/issues/48267 - } - - function showInfo(): void { - infoButton?.classList.add('selected'); - imageList?.classList.remove('hide-info'); - } - - function hideInfo(): void { - infoButton?.classList.remove('selected'); - imageList?.classList.add('hide-info'); - } - - function toggleInfo(): void { - infoButton?.classList.toggle('selected'); - imageList?.classList.toggle('hide-info'); - } - - // Event listeners - lightboxButtons.forEach((button) => { - button.addEventListener('click', () => { - // We use this class to prevent the main page from scrolling - document.documentElement.classList.add('lightbox-open'); - // Extract the element id for this image out of the button that was clicked on - const elementId = button.dataset.elementId; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue - (lightbox as any)?.showModal(); // See: https://github.com/microsoft/TypeScript/issues/48267 - // Find the image inside the lightbox and get its index - const imageWrapper: HTMLLIElement | null | undefined = elementId - ? lightbox?.querySelector( - `li[data-element-id="${elementId}"]`, - ) - : null; - const stringIndex: string = imageWrapper?.dataset.index ?? '1'; - const indexOfImageClicked = parseInt(stringIndex); - // Now we have the index of the image that was clicked, show it - // in the lightbox - select(indexOfImageClicked); - scrollTo(indexOfImageClicked); - loadAdjacentImages(indexOfImageClicked); - }); - }); - - lightbox?.addEventListener('keydown', (event) => { - switch (event.code) { - case 'Tab': { - // We're completely taking over tabbing to keep focus inside - // the dialog - event.preventDefault(); - switch (document.activeElement) { - case closeButton: - event.shiftKey - ? infoButton?.focus() - : previousButton?.focus(); - break; - case previousButton: - event.shiftKey - ? closeButton?.focus() - : nextButton?.focus(); - break; - case nextButton: - event.shiftKey - ? previousButton?.focus() - : infoButton?.focus(); - break; - case infoButton: - event.shiftKey - ? nextButton?.focus() - : closeButton?.focus(); - break; - default: - break; - } - break; - } - case 'ArrowLeft': - goBack(); - break; - case 'ArrowRight': - goForward(); - break; - case 'KeyI': - toggleInfo(); - break; - case 'KeyQ': - close(); - break; - case 'ArrowUp': - showInfo(); - break; - case 'ArrowDown': - hideInfo(); - break; - } - }); - - imageList?.addEventListener( - 'scroll', - libDebounce(() => { - const currentPosition = getPosition(); - select(currentPosition); - loadAdjacentImages(currentPosition); - }, 300), - ); - - closeButton?.addEventListener('click', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access , @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- because it's a known issue - (lightbox as any)?.close(); // See: https://github.com/microsoft/TypeScript/issues/48267 - }); - - previousButton?.addEventListener('click', () => { - goBack(); - }); - - nextButton?.addEventListener('click', () => { - goForward(); - }); - - infoButton?.addEventListener('click', () => { - toggleInfo(); - }); - - lightbox?.addEventListener('close', () => { - document.documentElement.classList.remove('lightbox-open'); - }); - }, []); - - // Don’t render anything - return null; -}; From 2730115dabf97cc5f3f82a1ff9cc190e5370e614 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Fri, 30 Dec 2022 13:34:26 +0000 Subject: [PATCH 008/218] feat: Use `isLightbox` to force larger image sources This solves a problem where images in liveblogs only had smaller sources --- .../src/web/components/Lightbox.tsx | 7 ++++--- dotcom-rendering/src/web/components/Picture.tsx | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index 44133ba3664..c3b6e3d7f65 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -426,9 +426,9 @@ export const Lightbox = ({ format, images }: Props) => { >
{ width={width} master={master} format={format} + isLightbox={true} />
diff --git a/dotcom-rendering/src/web/server/index.ts b/dotcom-rendering/src/web/server/index.ts index 22c6a8cf412..136089028e6 100644 --- a/dotcom-rendering/src/web/server/index.ts +++ b/dotcom-rendering/src/web/server/index.ts @@ -40,6 +40,7 @@ const enhanceCAPIType = (body: unknown): FEArticleType => { ? enhanceTableOfContents(data.format, enhancedBlocks) : undefined, imagesForLightbox: enhanceImagesForLightbox( + data.format, enhancedBlocks, data.mainMediaElements, ), From 9e1dcf032936cbcb6b5545ccd2c73ef138a5473c Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Mon, 2 Jan 2023 15:23:09 +0000 Subject: [PATCH 024/218] feat: Vertically centre images on mobile --- dotcom-rendering/src/web/components/Lightbox.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index d1aa4ab9e8b..ff879ada345 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -118,6 +118,10 @@ const imageStyles = (orientation: 'horizontal' | 'portrait') => { } } picture { + ${until.tablet} { + display: flex; + align-items: center; + } flex-grow: 1; ${from.tablet} { margin-right: ${space[5]}px; @@ -146,6 +150,10 @@ const imageStyles = (orientation: 'horizontal' | 'portrait') => { } } picture { + ${until.tablet} { + display: flex; + align-items: center; + } flex-grow: 1; ${from.tablet} { margin-right: ${space[5]}px; From 2324ece02a218c7ce0d97e8556f88a8682319095 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Mon, 2 Jan 2023 15:31:04 +0000 Subject: [PATCH 025/218] fix: Use default registry --- dotcom-rendering/package.json | 2 +- yarn.lock | 829 +++++++++++++++++++++++++++++++++- 2 files changed, 810 insertions(+), 21 deletions(-) diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index bb7f2215db1..a15a789b192 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -160,7 +160,7 @@ "curlyquotes": "^1.5.5", "cypress": "^10.3.0", "cypress-plugin-tab": "^1.0.5", - "cypress-real-events": "1.7.6", + "cypress-real-events": "^1.7.6", "cypress-wait-until": "^1.7.1", "cypress-webpack-preprocessor-v5": "^5.0.0-alpha.1", "desvg-loader": "^0.1.0", diff --git a/yarn.lock b/yarn.lock index 4b359fa9c81..e914a52cb6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2543,6 +2543,13 @@ dependencies: regenerator-runtime "^0.13.10" +"@babel/runtime@^7.18.3": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" @@ -2786,6 +2793,14 @@ "@emotion/sheet" "0.9.4" "@emotion/utils" "0.11.3" +"@emotion/css-prettifier@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@emotion/css-prettifier/-/css-prettifier-1.1.1.tgz#87f3104c057a55674ff464f9c4fdf7326847f0ea" + integrity sha512-dtiZLNN3dWP0mo/+VTPkzNrjp1wL6VRHOOSMn3XpQM4D/7xOVHEIQR6lT5Yj5Omun1REog5NFZ+5hxj+LtTIyQ== + dependencies: + "@emotion/memoize" "^0.8.0" + stylis "4.1.3" + "@emotion/css@^10.0.27", "@emotion/css@^10.0.9": version "10.0.27" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" @@ -2800,6 +2815,17 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== +"@emotion/jest@^11.3.0": + version "11.10.5" + resolved "https://registry.yarnpkg.com/@emotion/jest/-/jest-11.10.5.tgz#6aea1aec38e1c59e675702fa877644a6b1c18d05" + integrity sha512-LagosxybgisPlxfBGas9kGcNB5xTTX125WbjEVZiE3MEbb+dI4UYn0XIzrsilR8nUaQ5lH6yKUFrMmz7kB34vg== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/css-prettifier" "^1.1.1" + chalk "^4.1.0" + specificity "^0.4.1" + stylis "4.1.3" + "@emotion/memoize@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" @@ -2810,6 +2836,11 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== +"@emotion/memoize@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" + integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== + "@emotion/react@^11.4.1": version "11.5.0" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.5.0.tgz#19b5771bbfbda5e8517e948a2d9064810f0022bd" @@ -3092,6 +3123,18 @@ chalk "^2.0.1" slash "^2.0.0" +"@jest/console@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" + integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^26.6.2" + jest-util "^26.6.2" + slash "^3.0.0" + "@jest/core@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" @@ -3126,6 +3169,40 @@ slash "^2.0.0" strip-ansi "^5.0.0" +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/reporters" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-changed-files "^26.6.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + jest-watcher "^26.6.2" + micromatch "^4.0.2" + p-each-series "^2.1.0" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + "@jest/environment@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" @@ -3136,6 +3213,16 @@ "@jest/types" "^24.9.0" jest-mock "^24.9.0" +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== + dependencies: + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + "@jest/fake-timers@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" @@ -3156,6 +3243,27 @@ jest-util "^25.5.0" lolex "^5.0.0" +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== + dependencies: + "@jest/types" "^26.6.2" + "@sinonjs/fake-timers" "^6.0.1" + "@types/node" "*" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" + +"@jest/globals@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" + integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/types" "^26.6.2" + expect "^26.6.2" + "@jest/reporters@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" @@ -3183,6 +3291,38 @@ source-map "^0.6.0" string-length "^2.0.0" +"@jest/reporters@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" + integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.2.4" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^4.0.3" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.0.2" + jest-haste-map "^26.6.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^7.0.0" + optionalDependencies: + node-notifier "^8.0.0" + "@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" @@ -3192,6 +3332,15 @@ graceful-fs "^4.1.15" source-map "^0.6.0" +"@jest/source-map@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" + integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.4" + source-map "^0.6.0" + "@jest/test-result@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" @@ -3201,6 +3350,16 @@ "@jest/types" "^24.9.0" "@types/istanbul-lib-coverage" "^2.0.0" +"@jest/test-result@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" + integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + "@jest/test-sequencer@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" @@ -3211,6 +3370,17 @@ jest-runner "^24.9.0" jest-runtime "^24.9.0" +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== + dependencies: + "@jest/test-result" "^26.6.2" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + "@jest/transform@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56" @@ -3797,6 +3967,13 @@ dependencies: type-detect "4.0.8" +"@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@storybook/addon-actions@6.5.13": version "6.5.13" resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.5.13.tgz#84535dda78c7fe15fc61f19a23ed1440952f3c76" @@ -4961,6 +5138,17 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" +"@types/babel__core@^7.1.7": + version "7.1.20" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.20.tgz#e168cdd612c92a2d335029ed62ac94c95b362359" + integrity sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__generator@*": version "7.6.3" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.3.tgz#f456b4b2ce79137f768aa130d2423d2f0ccfaba5" @@ -4983,6 +5171,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/babel__traverse@^7.0.4": + version "7.18.3" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" + integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== + dependencies: + "@babel/types" "^7.3.0" + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -5359,7 +5554,7 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== -"@types/prettier@^2.6.0": +"@types/prettier@^2.0.0", "@types/prettier@^2.6.0": version "2.7.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== @@ -5419,6 +5614,13 @@ dependencies: "@types/react" "*" +"@types/react-test-renderer@^17.0.1": + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.2.tgz#5f800a39b12ac8d2a2149e7e1885215bcf4edbbf" + integrity sha512-+F1KONQTBHDBBhbHuT2GNydeMpPuviduXIVJRB7Y4nma4NR5DrTJfMMZ+jbhEHbpwL+Uqhs1WXh4KHiyrtYTPg== + dependencies: + "@types/react" "^17" + "@types/react@*": version "17.0.37" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" @@ -5493,7 +5695,7 @@ "@types/serve-static@*": version "1.15.0" - resolved "https://eurostar.jfrog.io/eurostar/api/npm/npm/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== dependencies: "@types/mime" "*" @@ -5521,6 +5723,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + "@types/tapable@^1.0.5": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" @@ -5589,7 +5796,7 @@ "@types/webpack@^4.41.26", "@types/webpack@^4.41.8", "@types/webpack@^5.28.0": version "5.28.0" - resolved "https://eurostar.jfrog.io/eurostar/api/npm/npm/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03" integrity sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w== dependencies: "@types/node" "*" @@ -6626,7 +6833,7 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-regex@^5.0.1: +ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -7055,6 +7262,20 @@ babel-jest@^24.9.0: chalk "^2.4.2" slash "^2.0.0" +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== + dependencies: + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/babel__core" "^7.1.7" + babel-plugin-istanbul "^6.0.0" + babel-preset-jest "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + slash "^3.0.0" + babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -7150,6 +7371,16 @@ babel-plugin-jest-hoist@^24.9.0: dependencies: "@types/babel__traverse" "^7.0.6" +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + babel-plugin-jest-hoist@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" @@ -7285,6 +7516,14 @@ babel-preset-jest@^24.9.0: "@babel/plugin-syntax-object-rest-spread" "^7.0.0" babel-plugin-jest-hoist "^24.9.0" +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== + dependencies: + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" + babel-preset-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" @@ -7925,6 +8164,11 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + camelcase@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.1.tgz#250fd350cfd555d0d2160b1d51510eaf8326e86e" @@ -8002,6 +8246,11 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + character-entities-legacy@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" @@ -8144,6 +8393,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +cjs-module-lexer@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" + integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -8252,6 +8506,15 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -8313,6 +8576,11 @@ collapse-white-space@^1.0.2: resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -8927,9 +9195,9 @@ cypress-plugin-tab@^1.0.5: dependencies: ally.js "^1.4.1" -cypress-real-events@1.7.6: +cypress-real-events@^1.7.6: version "1.7.6" - resolved "https://eurostar.jfrog.io/eurostar/api/npm/npm/cypress-real-events/-/cypress-real-events-1.7.6.tgz#6f17e0b2ceea1d6dc60f6737d8f84cc517bbbb4c" + resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.6.tgz#6f17e0b2ceea1d6dc60f6737d8f84cc517bbbb4c" integrity sha512-yP6GnRrbm6HK5q4DH6Nnupz37nOfZu/xn1xFYqsE2o4G73giPWQOdu6375QYpwfU1cvHNCgyD2bQ2hPH9D7NMw== cypress-wait-until@^1.7.1: @@ -9301,6 +9569,11 @@ detect-newline@^2.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" @@ -9326,6 +9599,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -9641,6 +9919,11 @@ emitter-listener@^1.1.1: dependencies: shimmer "^1.2.0" +emittery@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" + integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -10450,7 +10733,7 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w== -execa@4.1.0: +execa@4.1.0, execa@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== @@ -10542,6 +10825,18 @@ expect@^24.9.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" +expect@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== + dependencies: + "@jest/types" "^26.6.2" + ansi-styles "^4.0.0" + jest-get-type "^26.3.0" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + express@^4.17.1, express@^4.17.3: version "4.17.3" resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" @@ -12547,7 +12842,7 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" -is-core-module@^2.10.0: +is-core-module@^2.10.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== @@ -13011,6 +13306,16 @@ istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: istanbul-lib-coverage "^2.0.5" semver "^6.0.0" +istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + istanbul-lib-instrument@^5.0.4: version "5.1.0" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a" @@ -13051,6 +13356,15 @@ istanbul-lib-source-maps@^3.0.1: rimraf "^2.6.3" source-map "^0.6.1" +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + istanbul-reports@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931" @@ -13088,6 +13402,15 @@ jest-changed-files@^24.9.0: execa "^1.0.0" throat "^4.0.0" +jest-changed-files@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" + integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== + dependencies: + "@jest/types" "^26.6.2" + execa "^4.0.0" + throat "^5.0.0" + jest-cli@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" @@ -13107,6 +13430,25 @@ jest-cli@^24.9.0: realpath-native "^1.1.0" yargs "^13.3.0" +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== + dependencies: + "@jest/core" "^26.6.3" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + import-local "^3.0.2" + is-ci "^2.0.0" + jest-config "^26.6.3" + jest-util "^26.6.2" + jest-validate "^26.6.2" + prompts "^2.0.1" + yargs "^15.4.1" + jest-config@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" @@ -13130,6 +13472,30 @@ jest-config@^24.9.0: pretty-format "^24.9.0" realpath-native "^1.1.0" +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^26.6.3" + "@jest/types" "^26.6.2" + babel-jest "^26.6.3" + chalk "^4.0.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.4" + jest-environment-jsdom "^26.6.2" + jest-environment-node "^26.6.2" + jest-get-type "^26.3.0" + jest-jasmine2 "^26.6.3" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + micromatch "^4.0.2" + pretty-format "^26.6.2" + jest-diff@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" @@ -13140,6 +13506,16 @@ jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-diff@^27.0.0, jest-diff@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" @@ -13157,6 +13533,13 @@ jest-docblock@^24.3.0: dependencies: detect-newline "^2.1.0" +jest-docblock@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" + integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== + dependencies: + detect-newline "^3.0.0" + jest-each@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" @@ -13168,6 +13551,17 @@ jest-each@^24.9.0: jest-util "^24.9.0" pretty-format "^24.9.0" +jest-each@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" + integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + jest-get-type "^26.3.0" + jest-util "^26.6.2" + pretty-format "^26.6.2" + jest-environment-jsdom-sixteen@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/jest-environment-jsdom-sixteen/-/jest-environment-jsdom-sixteen-1.0.3.tgz#e222228fac537ef15cca5ad470b19b47d9690165" @@ -13190,6 +13584,19 @@ jest-environment-jsdom@^24.9.0: jest-util "^24.9.0" jsdom "^11.5.1" +jest-environment-jsdom@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" + integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jsdom "^16.4.0" + jest-environment-node@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" @@ -13201,11 +13608,28 @@ jest-environment-node@^24.9.0: jest-mock "^24.9.0" jest-util "^24.9.0" +jest-environment-node@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jest-get-type@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== +jest-get-type@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" + integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== + jest-get-type@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" @@ -13293,6 +13717,30 @@ jest-jasmine2@^24.9.0: pretty-format "^24.9.0" throat "^4.0.0" +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^26.6.2" + is-generator-fn "^2.0.0" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" + throat "^5.0.0" + jest-leak-detector@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" @@ -13301,6 +13749,14 @@ jest-leak-detector@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-leak-detector@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" + integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== + dependencies: + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-matcher-utils@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" @@ -13311,6 +13767,16 @@ jest-matcher-utils@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== + dependencies: + chalk "^4.0.0" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-matcher-utils@^27.0.0: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" @@ -13349,6 +13815,21 @@ jest-message-util@^25.5.0: slash "^3.0.0" stack-utils "^1.0.1" +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.2" + pretty-format "^26.6.2" + slash "^3.0.0" + stack-utils "^2.0.2" + jest-mock@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" @@ -13363,11 +13844,24 @@ jest-mock@^25.1.0, jest-mock@^25.5.0: dependencies: "@jest/types" "^25.5.0" +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-pnp-resolver@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" @@ -13392,6 +13886,15 @@ jest-resolve-dependencies@^24.9.0: jest-regex-util "^24.3.0" jest-snapshot "^24.9.0" +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== + dependencies: + "@jest/types" "^26.6.2" + jest-regex-util "^26.0.0" + jest-snapshot "^26.6.2" + jest-resolve@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" @@ -13403,6 +13906,20 @@ jest-resolve@^24.9.0: jest-pnp-resolver "^1.2.1" realpath-native "^1.1.0" +jest-resolve@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" + integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + jest-pnp-resolver "^1.2.2" + jest-util "^26.6.2" + read-pkg-up "^7.0.1" + resolve "^1.18.1" + slash "^3.0.0" + jest-runner@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" @@ -13428,6 +13945,32 @@ jest-runner@^24.9.0: source-map-support "^0.5.6" throat "^4.0.0" +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.7.1" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-docblock "^26.0.0" + jest-haste-map "^26.6.2" + jest-leak-detector "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + jest-runtime "^26.6.3" + jest-util "^26.6.2" + jest-worker "^26.6.2" + source-map-support "^0.5.6" + throat "^5.0.0" + jest-runtime@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" @@ -13457,6 +14000,39 @@ jest-runtime@^24.9.0: strip-bom "^3.0.0" yargs "^13.3.0" +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/globals" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + cjs-module-lexer "^0.6.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + slash "^3.0.0" + strip-bom "^4.0.0" + yargs "^15.4.1" + jest-serializer@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" @@ -13497,6 +14073,28 @@ jest-snapshot@^24.9.0: pretty-format "^24.9.0" semver "^6.2.0" +jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.0.0" + chalk "^4.0.0" + expect "^26.6.2" + graceful-fs "^4.2.4" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + natural-compare "^1.4.0" + pretty-format "^26.6.2" + semver "^7.3.2" + jest-teamcity-reporter@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/jest-teamcity-reporter/-/jest-teamcity-reporter-0.9.0.tgz#a9f337a928a14e7e84163817456b930ecf7cbce8" @@ -13531,7 +14129,7 @@ jest-util@^25.1.0, jest-util@^25.5.0: is-ci "^2.0.0" make-dir "^3.0.0" -jest-util@^26.6.2: +jest-util@^26.1.0, jest-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== @@ -13567,6 +14165,18 @@ jest-validate@^24.9.0: leven "^3.1.0" pretty-format "^24.9.0" +jest-validate@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" + integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== + dependencies: + "@jest/types" "^26.6.2" + camelcase "^6.0.0" + chalk "^4.0.0" + jest-get-type "^26.3.0" + leven "^3.1.0" + pretty-format "^26.6.2" + jest-watcher@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" @@ -13580,6 +14190,19 @@ jest-watcher@^24.9.0: jest-util "^24.9.0" string-length "^2.0.0" +jest-watcher@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" + integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== + dependencies: + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^26.6.2" + string-length "^4.0.1" + jest-worker@^24.6.0, jest-worker@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" @@ -13623,6 +14246,15 @@ jest@^24.9.0: import-local "^2.0.0" jest-cli "^24.9.0" +jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== + dependencies: + "@jest/core" "^26.6.3" + import-local "^3.0.2" + jest-cli "^26.6.3" + joi@^17.4.0: version "17.4.2" resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.2.tgz#02f4eb5cf88e515e614830239379dcbbe28ce7f7" @@ -13726,7 +14358,7 @@ jsdom@^11.5.1: ws "^5.2.0" xml-name-validator "^3.0.0" -jsdom@^16.2.1: +jsdom@^16.2.1, jsdom@^16.4.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== @@ -14328,7 +14960,7 @@ lodash.uniq@4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -15103,7 +15735,7 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5: dependencies: minimist "^1.2.5" -mkdirp@1.0.4, mkdirp@^1.0.3, mkdirp@^1.0.4: +mkdirp@1.0.4, mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -15350,6 +15982,18 @@ node-notifier@^5.4.2: shellwords "^0.1.1" which "^1.3.0" +node-notifier@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5" + integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg== + dependencies: + growly "^1.3.0" + is-wsl "^2.2.0" + semver "^7.3.2" + shellwords "^0.1.1" + uuid "^8.3.0" + which "^2.0.2" + node-releases@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" @@ -15787,6 +16431,11 @@ p-each-series@^1.0.0: dependencies: p-reduce "^1.0.0" +p-each-series@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" + integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA== + p-event@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" @@ -16707,6 +17356,16 @@ pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" @@ -17151,11 +17810,16 @@ react-inspector@^5.1.0: is-dom "^1.0.0" prop-types "^15.0.0" -react-is@17.0.2, react-is@^17.0.1: +react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0": + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -17185,6 +17849,24 @@ react-select@^3.2.0: react-input-autosize "^3.0.0" react-transition-group "^4.3.0" +react-shallow-renderer@^16.13.1: + version "16.15.0" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" + integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0 || ^18.0.0" + +react-test-renderer@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" + integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== + dependencies: + object-assign "^4.1.1" + react-is "^17.0.2" + react-shallow-renderer "^16.13.1" + scheduler "^0.20.2" + react-transition-group@^4.3.0: version "4.4.2" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" @@ -17375,7 +18057,7 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.10: +regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.11: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== @@ -17767,6 +18449,15 @@ resolve@1.x, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, is-core-module "^2.2.0" path-parse "^1.0.6" +resolve@^1.18.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.22.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" @@ -18063,6 +18754,13 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +semver@7.x: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + semver@^7.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" @@ -18599,6 +19297,13 @@ stack-utils@^1.0.1: dependencies: escape-string-regexp "^2.0.0" +stack-utils@^2.0.2: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + stackframe@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303" @@ -18718,6 +19423,14 @@ string-length@^2.0.0: astral-regex "^1.0.0" strip-ansi "^4.0.0" +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -19048,6 +19761,11 @@ stylelint@^13.13.1: v8-compile-cache "^2.3.0" write-file-atomic "^3.0.3" +stylis@4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" + integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== + stylis@^4.0.10, stylis@^4.0.3: version "4.0.10" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240" @@ -19105,6 +19823,14 @@ supports-color@^9.2.1: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.2.1.tgz#599dc9d45acf74c6176e0d880bab1d7d718fe891" integrity sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ== +supports-hyperlinks@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -19236,6 +19962,14 @@ term-size@^2.1.0: resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + terser-webpack-plugin@^1.4.3: version "1.4.5" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" @@ -19329,6 +20063,11 @@ throat@^4.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= +throat@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" + integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== + throttleit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" @@ -19574,6 +20313,22 @@ ts-jest@^24.3.0: semver "^5.5" yargs-parser "10.x" +ts-jest@^26.5.3: + version "26.5.6" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.6.tgz#c32e0746425274e1dfe333f43cd3c800e014ec35" + integrity sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + jest-util "^26.1.0" + json5 "2.x" + lodash "4.x" + make-error "1.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "20.x" + ts-loader@^9.3.0: version "9.3.0" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.0.tgz#980f4dbfb60e517179e15e10ed98e454b132159f" @@ -19703,7 +20458,7 @@ type-detect@4.0.8: type-fest@2.12.2, type-fest@^0.18.0, type-fest@^0.20.2, type-fest@^0.21.3, type-fest@^0.6.0, type-fest@^0.8.1, type-fest@^1.4.0, type-fest@^2.8.0: version "2.19.0" - resolved "https://eurostar.jfrog.io/eurostar/api/npm/npm/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== type-is@~1.6.18: @@ -20157,7 +20912,7 @@ uuid@^3.2.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.2: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -20172,6 +20927,15 @@ v8-compile-cache@^2.3.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +v8-to-istanbul@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1" + integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + v8-to-istanbul@^8.0.0: version "8.1.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.0.tgz#0aeb763894f1a0a1676adf8a8b7612a38902446c" @@ -21061,6 +21825,11 @@ yargs-parser@10.x: dependencies: camelcase "^4.1.0" +yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3, yargs-parser@^20.2.4, yargs-parser@^20.2.7: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" @@ -21069,10 +21838,13 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2, yargs-parser@^20.2.3, yargs-parser@^20.2.4, yargs-parser@^20.2.7: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" yargs-parser@^21.0.0: version "21.1.1" @@ -21095,6 +21867,23 @@ yargs@^13.3.0: y18n "^4.0.0" yargs-parser "^13.1.2" +yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yargs@^16.1.1, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" From 1e21dc4e51b46da98ad8fc229e3257905dcb5540 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Mon, 2 Jan 2023 15:33:21 +0000 Subject: [PATCH 026/218] fix: Remove unused import --- dotcom-rendering/src/model/enhanceImagesForLightbox.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dotcom-rendering/src/model/enhanceImagesForLightbox.ts b/dotcom-rendering/src/model/enhanceImagesForLightbox.ts index 0631fe51e01..759702a1d19 100644 --- a/dotcom-rendering/src/model/enhanceImagesForLightbox.ts +++ b/dotcom-rendering/src/model/enhanceImagesForLightbox.ts @@ -1,8 +1,4 @@ -import type { - CAPIElement, - EnhancedImageForLightbox, - ImageBlockElement, -} from '../types/content'; +import type { CAPIElement, EnhancedImageForLightbox } from '../types/content'; export const enhanceImagesForLightbox = ( format: CAPIFormat, From f7e69aef6fb7ca97bca0a97edd3edbd10d3262bf Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Mon, 2 Jan 2023 15:58:39 +0000 Subject: [PATCH 027/218] test: Add a story for when the published date is shown --- .../src/web/components/Lightbox.stories.tsx | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.stories.tsx b/dotcom-rendering/src/web/components/Lightbox.stories.tsx index c4de24abe40..4f465a01b80 100644 --- a/dotcom-rendering/src/web/components/Lightbox.stories.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.stories.tsx @@ -69,8 +69,7 @@ function showModal() { function showInfo() { const lightbox = document.querySelector('#gu-lightbox'); - const imageList = lightbox?.querySelector('ul'); - imageList?.classList.remove('hide-info'); + lightbox?.classList.remove('hide-info'); } export const Default = () => { @@ -141,6 +140,32 @@ export const WithRating = () => { ); }; +export const WhenLiveBlog = () => { + useEffect(() => { + showModal(); + showInfo(); + }); + return ( + + ); +}; + export const WithEverything = () => { useEffect(() => { showModal(); From a73074d6c67c2fecb580f9a4b02d8aaef3bd8a1f Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Mon, 2 Jan 2023 15:59:28 +0000 Subject: [PATCH 028/218] feat: Hide the nav controls on mobile --- dotcom-rendering/src/web/components/Lightbox.tsx | 15 +++++++++++++-- .../web/components/LightboxButton.importable.tsx | 6 +++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index ff879ada345..4f0a2a8f257 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -40,8 +40,17 @@ const dialogStyles = css` background-color: ${neutral[10]}; } - ul.hide-info aside { - display: none; + &.hide-info { + /* Always hide the info aside when the hide-info class exists on the dialog element */ + aside { + display: none; + } + ${until.tablet} { + /* Also hide the nav controls when on mobile */ + nav { + display: none; + } + } } button.selected { @@ -63,6 +72,8 @@ const navStyles = css` flex-direction: column; ${until.tablet} { flex-direction: row; + position: absolute; + width: 100%; } ${from.tablet} { padding-top: ${space[3]}px; diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx index 8338177c200..01c6765c0b7 100644 --- a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -153,18 +153,18 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { function showInfo(): void { infoButton?.classList.add('selected'); - imageList?.classList.remove('hide-info'); + lightbox.classList.remove('hide-info'); storage.local.set('gu.prefs.lightbox-hideinfo', false); } function hideInfo(): void { infoButton?.classList.remove('selected'); - imageList?.classList.add('hide-info'); + lightbox.classList.add('hide-info'); storage.local.set('gu.prefs.lightbox-hideinfo', true); } function toggleInfo(): void { - if (imageList?.classList.contains('hide-info')) { + if (lightbox.classList.contains('hide-info')) { showInfo(); } else { hideInfo(); From 8df53e0f0e42b61a0e63eb59bc9da28b549c70ad Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Mon, 2 Jan 2023 16:05:34 +0000 Subject: [PATCH 029/218] fix: Keep nav controls clickable --- dotcom-rendering/src/web/components/Lightbox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index 4f0a2a8f257..060ebab6926 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -73,6 +73,7 @@ const navStyles = css` ${until.tablet} { flex-direction: row; position: absolute; + z-index: 1; width: 100%; } ${from.tablet} { From 679863f576a3d05167551772f85e4feabce1ca92 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Mon, 2 Jan 2023 16:15:42 +0000 Subject: [PATCH 030/218] refactor: Rename `selected` > `active` --- dotcom-rendering/src/web/components/Lightbox.tsx | 2 +- .../src/web/components/LightboxButton.importable.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index 060ebab6926..05ce58ad14d 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -53,7 +53,7 @@ const dialogStyles = css` } } - button.selected { + button.active { background-color: ${neutral[46]}; } `; diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx index 01c6765c0b7..5e54f28336e 100644 --- a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -65,10 +65,10 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { } function pulseButton(button: HTMLButtonElement): void { - button.classList.add('selected'); + button.classList.add('active'); window.setTimeout(() => { - button.classList.remove('selected'); + button.classList.remove('active'); }, 75); } @@ -152,13 +152,13 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { } function showInfo(): void { - infoButton?.classList.add('selected'); + infoButton?.classList.add('active'); lightbox.classList.remove('hide-info'); storage.local.set('gu.prefs.lightbox-hideinfo', false); } function hideInfo(): void { - infoButton?.classList.remove('selected'); + infoButton?.classList.remove('active'); lightbox.classList.add('hide-info'); storage.local.set('gu.prefs.lightbox-hideinfo', true); } From 41d52be412355de2f4376d83ca0158f321603995 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Mon, 2 Jan 2023 16:29:36 +0000 Subject: [PATCH 031/218] feat: Improve vertical centering --- dotcom-rendering/src/web/components/Lightbox.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index 05ce58ad14d..fa89ea8a326 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -114,11 +114,13 @@ const imageStyles = (orientation: 'horizontal' | 'portrait') => { img { object-fit: contain; object-position: top; + ${until.tablet} { + object-position: center; + } width: auto; max-width: 100%; margin-left: auto; margin-right: auto; - margin-top: ${space[3]}px; ${from.tablet} { margin-top: ${space[5]}px; } From 882ca264b17005e59b1b02c143f832e78174d24a Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Tue, 3 Jan 2023 20:56:03 +0000 Subject: [PATCH 032/218] feat: Position images better --- dotcom-rendering/src/web/components/Lightbox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index fa89ea8a326..d180f92596b 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -128,7 +128,7 @@ const imageStyles = (orientation: 'horizontal' | 'portrait') => { height: calc(100vh - 90px); } ${from.tablet} { - height: calc(100vh - 24px); + height: calc(100vh - 40px); } } picture { @@ -160,7 +160,7 @@ const imageStyles = (orientation: 'horizontal' | 'portrait') => { max-height: calc(100vh - 90px); } ${from.tablet} { - max-height: calc(100vh - 24px); + max-height: calc(100vh - 40px); } } picture { From 8a43e08d5a6a25539671751d8edf4ca1aa25ee25 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Tue, 3 Jan 2023 21:05:06 +0000 Subject: [PATCH 033/218] feat: Simplify by removing Posted text (but keep for screen readers) --- .../src/web/components/Lightbox.tsx | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index d180f92596b..386d5e6384a 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -533,38 +533,36 @@ export const Lightbox = ({ format, images }: Props) => { /> {!!image.blockId && !!image.firstPublished && ( -
- Posted{' '} - - - -
+ Original post + published{' '} + + {timeAgo( + image.firstPublished, + )} + + )} From 19539189d3b105d5ab853d0a7dc885ff9fa9c955 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Tue, 3 Jan 2023 21:09:18 +0000 Subject: [PATCH 034/218] refactor: Smaller buttons on mobile --- .../components/LightboxButton.importable.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx index 5e54f28336e..72625efb799 100644 --- a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -1,6 +1,11 @@ import { css } from '@emotion/react'; import { storage } from '@guardian/libs'; -import { neutral, space, visuallyHidden } from '@guardian/source-foundations'; +import { + from, + neutral, + space, + visuallyHidden, +} from '@guardian/source-foundations'; import { SvgArrowExpand } from '@guardian/source-react-components'; import libDebounce from 'lodash.debounce'; import { useEffect } from 'react'; @@ -16,8 +21,8 @@ function decideSize(role: RoleType) { case 'halfWidth': case 'supporting': { return css` - height: 30px; - width: 30px; + height: 32px; + width: 32px; `; } case 'inline': @@ -25,8 +30,12 @@ function decideSize(role: RoleType) { case 'immersive': default: { return css` - height: 44px; - width: 44px; + height: 32px; + width: 32px; + ${from.tablet} { + height: 44px; + width: 44px; + } `; } } From b10991b5571268d6f45561e68759be69012529ca Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Tue, 3 Jan 2023 21:09:59 +0000 Subject: [PATCH 035/218] refactor: Less padding to align with (main media) caption button better --- .../src/web/components/LightboxButton.importable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx index 72625efb799..640fc0e6af1 100644 --- a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -321,8 +321,8 @@ export const LightboxButton = ({ elementId, role }: Props) => { css={[ css` position: absolute; - top: 3px; - right: 3px; + top: 0px; + right: 0px; svg { margin-top: 4px; fill: ${neutral[100]}; From a5441140594e63f57977429090e4d7636f2486f7 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Tue, 3 Jan 2023 21:17:44 +0000 Subject: [PATCH 036/218] feat: Prevent accidental navigation when scrolling --- dotcom-rendering/src/web/components/Lightbox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index 386d5e6384a..24e5f2c3f0e 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -96,6 +96,7 @@ const ulStyles = css` scroll-snap-type: x mandatory; overflow-x: scroll; scroll-behavior: auto; + overscroll-behavior: contain; ${from.tablet} { margin-left: ${space[5]}px; } From c9ce7371bee896d35d33b32278fcf012322cf560 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Tue, 3 Jan 2023 22:29:26 +0000 Subject: [PATCH 037/218] feat: A better, more dynamic, focus trapping solution --- .../components/LightboxButton.importable.tsx | 70 ++++++++++++------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx index 640fc0e6af1..d910f6424fa 100644 --- a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -67,10 +67,30 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { } // Functions + function getTabableElements(): HTMLElement[] { + return Array.from( + lightbox.querySelectorAll( + 'button:not([disabled]), a:not([disabled]), input:not([disabled]), select:not([disabled])', + ), + ); + } + function select(position: number): void { if (positionDisplay) { positionDisplay.innerHTML = position.toString(); } + // Mark this page as active + lightbox + .querySelector(`ul li[data-index="${position}"]`) + ?.removeAttribute('inert'); + // Mark all other pages as inert + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert + // We do this to prevent the browser tabbing into them + lightbox + .querySelectorAll(`ul li:not([data-index="${position}"])`) + .forEach((li) => { + li.setAttribute('inert', 'true'); + }); } function pulseButton(button: HTMLButtonElement): void { @@ -180,6 +200,12 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { } } + function isFocussed(element: HTMLElement): boolean { + return ( + document.activeElement?.classList.value === element.classList.value + ); + } + // Event listeners lightboxButtons.forEach((button) => { button.addEventListener('click', () => { @@ -212,32 +238,24 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { lightbox.addEventListener('keydown', (event) => { switch (event.code) { case 'Tab': { - // We're completely taking over tabbing to keep focus inside - // the dialog - event.preventDefault(); - switch (document.activeElement) { - case closeButton: - event.shiftKey - ? infoButton?.focus() - : previousButton?.focus(); - break; - case previousButton: - event.shiftKey - ? closeButton?.focus() - : nextButton?.focus(); - break; - case nextButton: - event.shiftKey - ? previousButton?.focus() - : infoButton?.focus(); - break; - case infoButton: - event.shiftKey - ? nextButton?.focus() - : closeButton?.focus(); - break; - default: - break; + const tabableElements = getTabableElements(); + const firstFocusableElement = tabableElements[0]; + const lastFocusableElement = + tabableElements[tabableElements.length - 1]; + + // This won't happen but we need to appease typescript + if (!firstFocusableElement || !lastFocusableElement) return; + + if (event.shiftKey) { + if (isFocussed(firstFocusableElement)) { + event.preventDefault(); + lastFocusableElement.focus(); + } + } else { + if (isFocussed(lastFocusableElement)) { + event.preventDefault(); + firstFocusableElement.focus(); + } } break; } From 00d136ff7ad84b6ce699b52a2d9fcec69b304f34 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Sat, 7 Jan 2023 10:38:16 +0000 Subject: [PATCH 038/218] feat: The lightbox now opens when clicking anywhere on an image --- .../components/LightboxButton.importable.tsx | 145 +++++++++++++----- 1 file changed, 108 insertions(+), 37 deletions(-) diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx index d910f6424fa..a51cea21ed5 100644 --- a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -8,7 +8,7 @@ import { } from '@guardian/source-foundations'; import { SvgArrowExpand } from '@guardian/source-react-components'; import libDebounce from 'lodash.debounce'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import type { RoleType } from '../../types/content'; type Props = { @@ -46,6 +46,8 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { const lightboxButtons = document.querySelectorAll( 'button.open-lightbox', ); + const overlays = + document.querySelectorAll('div.open-lightbox'); // Lightbox selectors const nextButton = lightbox.querySelector('button.next'); @@ -229,6 +231,14 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { }); }); + overlays.forEach((overlay) => { + overlay.addEventListener('click', (event) => { + (event.target as HTMLElement) + .querySelector('button') + ?.dispatchEvent(new MouseEvent('click')); + }); + }); + LIs.forEach((LI) => { LI.addEventListener('click', () => { toggleInfo(); @@ -321,6 +331,66 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { }); } +/** + * This overlay makes it possible to click anywhere on an image and the lightbox + * will hydrate/appear. + * + * How? + * ---- + * ```html + * + * + * + * + * + * ``` + * + * Both gu-island and ClickOverlay have click event listeners that will replay any + * click event to their children using synthetic events. The gu-island listener is + * set on page load which in turn triggers hydration for this file, adding event + * listeners to the ClickOverlay and button. + * + * The flow is: + * + * 1) Page loads, gu-island has a click event listener set + * 2) User clicks on the island (which wraps both button and overlay) + * 3) This file is hydrated, setting listeners on both the ClickOverlay and the button + * 4) gu-island fires a synthetic click event to the element originally clicked + * 5) If that was ClickOverlay, another synthetic event is sent to the button + * 6) If it was the button then the lightbox is opened + * + * Why? + * ---- + * This approach means that it is possible for images to be clickable without the + * requirement to hydrate images - we never want to serialize images into the page! It + * also means we keep the feature where the lightbox code is only downloaded on + * interaction. + * + * What about accessibility? + * ------------------------- + * The child button element is still the primary method for opening + * the lightbox and this is the element that we want assitive technologies like + * screen readers to interact with. This overlay is intentionally hidden from + * keyboards and screen readers and can be thought of as progressive enhancement + * for mouse and touch users. + * + */ +const ClickOverlay = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + export const LightboxButton = ({ elementId, role }: Props) => { useEffect(() => { const lightbox = @@ -331,42 +401,43 @@ export const LightboxButton = ({ elementId, role }: Props) => { // Don't show the button over thumbnails; they're too small if (role === 'thumbnail') return null; return ( - + + + View in fullscreen + + + ); }; From a7ca153e70de1bd6a2389797d031674010845d10 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Sat, 7 Jan 2023 11:07:01 +0000 Subject: [PATCH 039/218] fix: `getTabableElements` now only returns visible elements --- .../components/LightboxButton.importable.tsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx index a51cea21ed5..71dd87ccf63 100644 --- a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -69,12 +69,29 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { } // Functions + + /** + * Returns a list of all the html elements on the *active* page that can be tabbed to + * + * Any elements that are off screen, such as caption links for images that are not + * currently showing, are ignored + */ function getTabableElements(): HTMLElement[] { - return Array.from( - lightbox.querySelectorAll( - 'button:not([disabled]), a:not([disabled]), input:not([disabled]), select:not([disabled])', - ), + function getElements(parent: HTMLElement): HTMLElement[] { + return Array.from( + parent.querySelectorAll( + 'button:not([disabled]), a:not([disabled]), input:not([disabled]), select:not([disabled])', + ), + ); + } + const currentPosition = getPosition(); + const currentPage = lightbox.querySelector( + `li[data-index="${currentPosition}"]`, ); + const nav = lightbox.querySelector('nav'); + const elementsFromPage = currentPage ? getElements(currentPage) : []; + const elementsFromNav = nav ? getElements(nav) : []; + return [...elementsFromNav, ...elementsFromPage]; } function select(position: number): void { From 3b6049bb203f33fdc1f8e8d67087ea1d7d985328 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Sat, 7 Jan 2023 11:36:03 +0000 Subject: [PATCH 040/218] minor: A little more space for the nav --- dotcom-rendering/src/web/components/Lightbox.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index 24e5f2c3f0e..c3269063b10 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -78,6 +78,8 @@ const navStyles = css` } ${from.tablet} { padding-top: ${space[3]}px; + padding-left: ${space[4]}px; + padding-right: ${space[4]}px; height: 100vh; } color: white; @@ -229,7 +231,11 @@ const buttonStyles = css` svg { fill: ${neutral[100]}; } - margin: ${space[2]}px; + margin-top: ${space[2]}px; + margin-bottom: ${space[2]}px; + ${until.tablet} { + margin-left: ${space[2]}px; + } border-radius: 50%; height: 44px; width: 44px; @@ -305,6 +311,7 @@ const Selection = ({ ${from.tablet} { text-align: center; padding-top: 2.25rem; + margin-bottom: ${space[1]}px; } ${textSans.xsmall()}; color: ${neutral[86]}; From d796d39fb4c7da6cc19682f1058239357c20cbe2 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Sat, 7 Jan 2023 19:05:45 +0000 Subject: [PATCH 041/218] fix: Prevent focus being lost when clicking LIs --- .../src/web/components/LightboxButton.importable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx index 71dd87ccf63..e387a730ee2 100644 --- a/dotcom-rendering/src/web/components/LightboxButton.importable.tsx +++ b/dotcom-rendering/src/web/components/LightboxButton.importable.tsx @@ -257,8 +257,11 @@ function initialiseLightbox(lightbox: HTMLDialogElement) { }); LIs.forEach((LI) => { - LI.addEventListener('click', () => { + LI.addEventListener('mousedown', (event) => { toggleInfo(); + // We want to maintain focus so halt all further actions + event.preventDefault(); + event.stopPropagation(); }); }); From 81ee019adeb2a315144e795b5caabbea883aee77 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Sat, 7 Jan 2023 19:07:34 +0000 Subject: [PATCH 042/218] fix: Position these button SVGs better --- dotcom-rendering/src/web/components/Lightbox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index c3269063b10..6e40cf49af2 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -230,6 +230,7 @@ const figureStyles = css` const buttonStyles = css` svg { fill: ${neutral[100]}; + margin-top: 3px; } margin-top: ${space[2]}px; margin-bottom: ${space[2]}px; From b423b09c745a554cef293d9b3609724777bd4ff4 Mon Sep 17 00:00:00 2001 From: Oliver Lloyd Date: Sun, 8 Jan 2023 07:39:28 +0000 Subject: [PATCH 043/218] fix: The caption can sometimes container ul/li elements so be more specific --- dotcom-rendering/src/web/components/Lightbox.tsx | 2 +- .../src/web/components/LightboxButton.importable.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/web/components/Lightbox.tsx b/dotcom-rendering/src/web/components/Lightbox.tsx index 6e40cf49af2..8ebc933d889 100644 --- a/dotcom-rendering/src/web/components/Lightbox.tsx +++ b/dotcom-rendering/src/web/components/Lightbox.tsx @@ -436,7 +436,7 @@ export const Lightbox = ({ format, images }: Props) => { -
    + + >