diff --git a/apps-rendering/src/components/headerImage.tsx b/apps-rendering/src/components/headerImage.tsx index 922ec0597a7..d2556b5c591 100644 --- a/apps-rendering/src/components/headerImage.tsx +++ b/apps-rendering/src/components/headerImage.tsx @@ -2,17 +2,21 @@ import type { SerializedStyles } from '@emotion/react'; import { css } from '@emotion/react'; +import ImageDetails from '@guardian/common-rendering/src/components/imageDetails'; import Img from '@guardian/common-rendering/src/components/img'; import type { Sizes } from '@guardian/common-rendering/src/sizes'; import { remSpace } from '@guardian/src-foundations'; import { from } from '@guardian/src-foundations/mq'; import type { Format } from '@guardian/types'; import { Design, Display, some } from '@guardian/types'; -import HeaderImageCaption, { captionId } from 'components/headerImageCaption'; import type { Image } from 'image'; import type { FC } from 'react'; import { wideContentWidth } from 'styles'; +// ----- Setup ----- // + +const captionId = 'header-image-caption'; + // ----- Subcomponents ----- // interface CaptionProps { @@ -26,9 +30,11 @@ const Caption: FC = ({ format, image }: CaptionProps) => { return null; default: return ( - ); } diff --git a/apps-rendering/src/components/headerImageCaption.tsx b/apps-rendering/src/components/headerImageCaption.tsx deleted file mode 100644 index fb1f57b2a3c..00000000000 --- a/apps-rendering/src/components/headerImageCaption.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import type { SerializedStyles } from '@emotion/react'; -import { css } from '@emotion/react'; -import { remSpace } from '@guardian/src-foundations'; -import { from } from '@guardian/src-foundations/mq'; -import { brandAlt, neutral } from '@guardian/src-foundations/palette'; -import { textSans } from '@guardian/src-foundations/typography'; -import { SvgCamera } from '@guardian/src-icons'; -import type { Option } from '@guardian/types'; -import { map, withDefault } from '@guardian/types'; -import { pipe } from 'lib'; -import type { FC, ReactElement } from 'react'; -import { darkModeCss, wideContentWidth } from 'styles'; - -const captionId = 'header-image-caption'; - -const HeaderImageCaptionStyles = ( - iconBackgroundColor?: string, -): SerializedStyles => css` - summary { - display: block; - text-align: center; - background-color: ${iconBackgroundColor - ? iconBackgroundColor - : brandAlt[400]}; - width: 34px; - height: 34px; - position: absolute; - bottom: 8px; - right: 8px; - border-radius: 100%; - outline: none; - - &::-webkit-details-marker { - display: none; - } - - ${darkModeCss` - background-color: ${neutral[60]}; - opacity: .7; - `} - } - - details[open] { - min-height: 44px; - max-height: 999px; - background-color: rgba(0, 0, 0, 0.8); - padding: ${remSpace[3]}; - overflow: hidden; - padding-right: ${remSpace[12]}; - z-index: 1; - color: ${neutral[100]}; - ${textSans.small()}; - box-sizing: border-box; - - ${darkModeCss` - color: ${neutral[60]}; - `} - } - - position: absolute; - left: 0; - right: 0; - bottom: 0; - - ${from.wide} { - width: ${wideContentWidth}px; - } -`; - -const svgStyle = (iconColor?: string): SerializedStyles => css` - line-height: 32px; - font-size: 0; - svg { - width: 75%; - height: 75%; - margin: 12.5%; - } - path { - fill: ${iconColor ? iconColor : neutral[7]}; - } -`; - -interface Props { - caption: Option; - credit: Option; - styles?: SerializedStyles; - iconColor?: string; - iconBackgroundColor?: string; -} - -const HeaderImageCaption: FC = ({ - caption, - credit, - styles, - iconColor, - iconBackgroundColor, -}: Props) => - pipe( - caption, - map((cap) => ( -
-
- - - - Click to see figure caption - - - - {cap} {withDefault('')(credit)} - -
-
- )), - withDefault(null), - ); - -export default HeaderImageCaption; - -export { captionId }; diff --git a/apps-rendering/src/lib.ts b/apps-rendering/src/lib.ts index 6e08370320a..b2d7f331d0d 100644 --- a/apps-rendering/src/lib.ts +++ b/apps-rendering/src/lib.ts @@ -1,5 +1,6 @@ // ----- Imports ----- // +import { maybeRender, pipe } from '@guardian/common-rendering/src/lib'; import type { Option, Result } from '@guardian/types'; import { err, @@ -12,7 +13,6 @@ import { some, withDefault, } from '@guardian/types'; -import type { ReactElement } from 'react'; // ----- Functions ----- // @@ -21,29 +21,6 @@ const compose = (a: A): C => f(g(a)); -function pipe(a: A, f: (_a: A) => B): B; -function pipe(a: A, f: (_a: A) => B, g: (_b: B) => C): C; -function pipe( - a: A, - f: (_a: A) => B, - g: (_b: B) => C, - h: (_c: C) => D, -): D; -function pipe( - a: A, - f: (_a: A) => B, - g?: (_b: B) => C, - h?: (_c: C) => D, -): unknown { - if (g !== undefined && h !== undefined) { - return h(g(f(a))); - } else if (g !== undefined) { - return g(f(a)); - } - - return f(a); -} - const identity = (a: A): A => a; // The nodeType for ELEMENT_NODE has the value 1. @@ -77,11 +54,6 @@ function errorToString(error: unknown, fallback: string): string { const isObject = (a: unknown): a is Record => typeof a === 'object' && a !== null; -const maybeRender = ( - oa: Option, - f: (a: A) => ReactElement | null, -): ReactElement | null => fold(f, null)(oa); - function handleErrors(response: Response): Response | never { if (!response.ok) { throw Error(response.statusText); diff --git a/common-rendering/package.json b/common-rendering/package.json index a6f1f6432af..f0fcf85546c 100644 --- a/common-rendering/package.json +++ b/common-rendering/package.json @@ -17,6 +17,7 @@ "devDependencies": { "@emotion/jest": "^11.3.0", "@types/jest": "^26.0.20", + "@types/react-test-renderer": "^17.0.1", "jest": "^26.6.3", "react-test-renderer": "^17.0.1", "ts-jest": "^26.5.3" diff --git a/common-rendering/src/components/imageDetails.stories.tsx b/common-rendering/src/components/imageDetails.stories.tsx new file mode 100644 index 00000000000..6c09f6eaaf7 --- /dev/null +++ b/common-rendering/src/components/imageDetails.stories.tsx @@ -0,0 +1,27 @@ +// ----- Imports ----- // + +import { some } from '@guardian/types'; +import type { FC } from 'react'; + +import ImageDetails from './imageDetails'; + +// ----- Stories ----- // + +const Default: FC = () => + + +// ----- Exports ----- // + +export default { + component: ImageDetails, + title: 'Common/Components/ImageDetails', +} + +export { + Default, +} diff --git a/apps-rendering/src/components/headerImageCaption.test.tsx b/common-rendering/src/components/imageDetails.test.tsx similarity index 73% rename from apps-rendering/src/components/headerImageCaption.test.tsx rename to common-rendering/src/components/imageDetails.test.tsx index 2eafe64f8c8..16e8dbe5ba9 100644 --- a/apps-rendering/src/components/headerImageCaption.test.tsx +++ b/common-rendering/src/components/imageDetails.test.tsx @@ -1,16 +1,25 @@ +// ----- Imports ----- // + import { matchers } from '@emotion/jest'; import { some } from '@guardian/types'; -import HeaderImageCaption, { captionId } from 'components/headerImageCaption'; +import ImageDetails from './imageDetails'; import renderer from 'react-test-renderer'; +// ----- Setup ----- // + expect.extend(matchers); +const captionId = 'header-image-caption'; + +// ----- Tests ----- // describe('HeaderImageCaption component renders as expected', () => { it('Formats the Caption correctly', () => { const headerImageCaption = renderer.create( - , ); diff --git a/common-rendering/src/components/imageDetails.tsx b/common-rendering/src/components/imageDetails.tsx new file mode 100644 index 00000000000..911f529e060 --- /dev/null +++ b/common-rendering/src/components/imageDetails.tsx @@ -0,0 +1,115 @@ +// ----- Imports ----- // + +import type { SerializedStyles } from '@emotion/react'; +import { css } from '@emotion/react'; +import { remSpace } from '@guardian/src-foundations'; +import { brandAlt, neutral } from '@guardian/src-foundations/palette'; +import { textSans } from '@guardian/src-foundations/typography'; +import { SvgCamera } from '@guardian/src-icons'; +import { Option, OptionKind } from '@guardian/types'; +import { withDefault } from '@guardian/types'; +import { darkModeCss } from '@guardian/common-rendering/src/lib'; +import type { FC } from 'react'; + +// ----- Component ----- // + +const styles = css` + position: absolute; + left: 0; + right: 0; + bottom: 0; +`; + +const detailsStyles = (supportsDarkMode: boolean): SerializedStyles => css` + &[open] { + min-height: 44px; + max-height: 999px; + background-color: rgba(0, 0, 0, 0.8); + padding: ${remSpace[3]}; + overflow: hidden; + padding-right: ${remSpace[12]}; + z-index: 1; + color: ${neutral[100]}; + ${textSans.small()}; + box-sizing: border-box; + + ${darkModeCss(supportsDarkMode)` + color: ${neutral[60]}; + `} + } +`; + +const iconStyles = (supportsDarkMode: boolean): SerializedStyles => css` + display: block; + text-align: center; + background-color: ${brandAlt[400]}; + width: 34px; + height: 34px; + position: absolute; + bottom: 8px; + right: 8px; + border-radius: 100%; + outline: none; + + &::-webkit-details-marker { + display: none; + } + + ${darkModeCss(supportsDarkMode)` + background-color: ${neutral[60]}; + opacity: .7; + `} +`; + +const svgStyles: SerializedStyles = css` + line-height: 32px; + font-size: 0; + + svg { + width: 75%; + height: 75%; + margin: 12.5%; + } + + path { + fill: ${neutral[7]}; + } +`; + +interface Props { + caption: Option; + credit: Option; + supportsDarkMode: boolean; + id: string; +} + +const ImageDetails: FC = ({ + caption, + credit, + supportsDarkMode, + id, +}: Props) => { + if (caption.kind === OptionKind.None && credit.kind === OptionKind.None) { + return null; + } + + return ( +
+
+ + + + Click to see figure caption + + + + {withDefault('')(caption)} {withDefault('')(credit)} + +
+
+ ); +} + +// ----- Exports ----- // + +export default ImageDetails; diff --git a/common-rendering/src/lib.ts b/common-rendering/src/lib.ts index 56672f9cf32..64acdc7f483 100644 --- a/common-rendering/src/lib.ts +++ b/common-rendering/src/lib.ts @@ -2,6 +2,9 @@ import type { SerializedStyles } from "@emotion/react"; import { css } from "@emotion/react"; +import type { Option } from '@guardian/types'; +import { map, withDefault } from '@guardian/types'; +import { ReactElement } from 'react'; // ----- Functions ----- // @@ -21,6 +24,35 @@ const darkModeCss = (supportsDarkMode: boolean) => ( ` : css``; +function pipe(a: A, f: (_a: A) => B): B; +function pipe(a: A, f: (_a: A) => B, g: (_b: B) => C): C; +function pipe( + a: A, + f: (_a: A) => B, + g: (_b: B) => C, + h: (_c: C) => D, +): D; +function pipe( + a: A, + f: (_a: A) => B, + g?: (_b: B) => C, + h?: (_c: C) => D, +): unknown { + if (g !== undefined && h !== undefined) { + return h(g(f(a))); + } else if (g !== undefined) { + return g(f(a)); + } + + return f(a); +} + +const maybeRender =
( + oa: Option, + f: (a: A) => ReactElement | null, +): ReactElement | null => + pipe(oa, map(f), withDefault(null)); + // ----- Exports ----- // -export { darkModeCss }; +export { darkModeCss, maybeRender, pipe }; diff --git a/yarn.lock b/yarn.lock index 841bf543aa0..654b5f351a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3897,7 +3897,7 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@17.0.1": +"@types/react-test-renderer@17.0.1", "@types/react-test-renderer@^17.0.1": version "17.0.1" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==