From ea44a9d6fdc8693f936c60bdc3b248a8253150e9 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 24 Oct 2023 22:15:59 -0700 Subject: [PATCH 01/29] Move main Visual type declaration into react-visual --- packages/next/src/index.ts | 6 +--- packages/next/src/types/nextVisualTypes.ts | 35 ++------------------ packages/react/src/index.ts | 5 +++ packages/react/src/types/reactVisualTypes.ts | 34 +++++++++++++++++++ packages/sanity-next/src/lib/urlBuilding.ts | 2 +- 5 files changed, 43 insertions(+), 39 deletions(-) create mode 100644 packages/react/src/types/reactVisualTypes.ts diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index 9d7cc01..b59cc4d 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -1,7 +1,3 @@ import NextVisual from './NextVisual' export default NextVisual -export { - NextVisualProps, - ObjectFit, - ObjectFitOption, -} from './types/nextVisualTypes' +export { NextVisualProps } from './types/nextVisualTypes' diff --git a/packages/next/src/types/nextVisualTypes.ts b/packages/next/src/types/nextVisualTypes.ts index c2c27f5..b6314bc 100644 --- a/packages/next/src/types/nextVisualTypes.ts +++ b/packages/next/src/types/nextVisualTypes.ts @@ -1,34 +1,3 @@ -import type { CSSProperties } from 'react' +import type { ReactVisualTypes } from '@react-visual/react' -export type NextVisualProps = { - - image?: string - video?: string - placeholderData?: string - - expand?: boolean - aspect?: number // An explict aspect ratio - width?: number | string - height?: number | string - fit?: ObjectFitOption | ObjectFit - position?: string - - priority?: boolean - sizes?: string - imageLoader?: Function - - paused?: boolean - - alt: string - - className?: string - style?: CSSProperties -} - -export type ObjectFitOption = 'cover' | 'contain' - -// Deprecated -export enum ObjectFit { - Cover = 'cover', - Contain = 'contain', -} +export type NextVisualProps = ReactVisualTypes diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index b0fd54a..1c2391e 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,3 +2,8 @@ import LazyVideo from './LazyVideo' import VisualWrapper from './VisualWrapper' import { collectDataAttributes } from './lib/attributes' export { LazyVideo, VisualWrapper, collectDataAttributes } +export { + ReactVisualTypes, + ObjectFit, + ObjectFitOption +} from './types/reactVisualTypes' diff --git a/packages/react/src/types/reactVisualTypes.ts b/packages/react/src/types/reactVisualTypes.ts new file mode 100644 index 0000000..c267b7f --- /dev/null +++ b/packages/react/src/types/reactVisualTypes.ts @@ -0,0 +1,34 @@ +import type { CSSProperties } from 'react' + +export type ReactVisualTypes = { + + image?: string + video?: string + placeholderData?: string + + expand?: boolean + aspect?: number // An explict aspect ratio + width?: number | string + height?: number | string + fit?: ObjectFitOption | ObjectFit + position?: string + + priority?: boolean + sizes?: string + imageLoader?: Function + + paused?: boolean + + alt: string + + className?: string + style?: CSSProperties +} + +export type ObjectFitOption = 'cover' | 'contain' + +// Deprecated +export enum ObjectFit { + Cover = 'cover', + Contain = 'contain', +} diff --git a/packages/sanity-next/src/lib/urlBuilding.ts b/packages/sanity-next/src/lib/urlBuilding.ts index 962bc32..dadac94 100644 --- a/packages/sanity-next/src/lib/urlBuilding.ts +++ b/packages/sanity-next/src/lib/urlBuilding.ts @@ -3,7 +3,7 @@ import { getFileAsset, type SanityFileSource } from '@sanity/asset-utils' import type { ImageUrlBuilder } from '@sanity/image-url/lib/types/builder' import type { ImageLoader, ImageLoaderProps } from 'next/image' import type { SanityImageSource } from '@sanity/image-url/lib/types/types' -import { ObjectFitOption } from '@react-visual/next' +import { ObjectFitOption } from '@react-visual/react' // Access ENV vars const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID as string, From 7b8b15ce85fa092bf7f11a18dc28acde930fdbd6 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 24 Oct 2023 22:16:18 -0700 Subject: [PATCH 02/29] Stub out ReactVisual initial tests --- .../cypress/component/ReactVisual.cy.tsx | 54 +++++++++++++++++++ packages/react/cypress/support/commands.ts | 10 ++++ packages/react/src/ReactVisual.tsx | 11 ++++ packages/react/src/index.ts | 3 ++ 4 files changed, 78 insertions(+) create mode 100644 packages/react/cypress/component/ReactVisual.cy.tsx create mode 100644 packages/react/src/ReactVisual.tsx diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx new file mode 100644 index 0000000..4877471 --- /dev/null +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -0,0 +1,54 @@ +import ReactVisual from '../../src' + +// Viewport sizes +const VW = Cypress.config('viewportWidth'), + VH = Cypress.config('viewportHeight') + +describe('no asset', () => { + + it('renders nothing', () => { + cy.mount() + cy.get('[data-cy=react-visual]').should('not.exist') + }) + +}) + +describe('fixed size', () => { + + it('renders image', () => { + cy.mount() + cy.get('img').hasDimensions(300, 200) + }) + + it('renders video', () => { + cy.mount() + cy.get('video').hasDimensions(300, 200) + cy.get('video').isPlaying() + }) + + it('renders image & video', () => { + cy.mount() + cy.get('[data-cy=next-visual]').hasDimensions(300, 200) + cy.get('img').hasDimensions(300, 200) + cy.get('video').hasDimensions(300, 200) + cy.get('video').isPlaying() + }) +}) diff --git a/packages/react/cypress/support/commands.ts b/packages/react/cypress/support/commands.ts index fcf31ee..6defc28 100644 --- a/packages/react/cypress/support/commands.ts +++ b/packages/react/cypress/support/commands.ts @@ -1,5 +1,15 @@ /// +// Asset that el has dimensions +Cypress.Commands.add('hasDimensions', + { prevSubject: true }, + (subject, width, height) => { + + expect(subject.width()).to.equal(width) + expect(subject.height()).to.equal(height) + return subject +}) + // Check that a video is playing // https://glebbahmutov.com/blog/test-video-play/ Cypress.Commands.add('isPlaying', diff --git a/packages/react/src/ReactVisual.tsx b/packages/react/src/ReactVisual.tsx new file mode 100644 index 0000000..8410b3f --- /dev/null +++ b/packages/react/src/ReactVisual.tsx @@ -0,0 +1,11 @@ +import type { ReactElement } from 'react' + +import { ReactVisualTypes } from './types/reactVisualTypes' + +export default function ReactVisual( + props: ReactVisualTypes +): ReactElement | null { + return ( +

Hey

+ ) +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 1c2391e..12a1ab9 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,6 +1,9 @@ +import ReactVisual from './ReactVisual' import LazyVideo from './LazyVideo' import VisualWrapper from './VisualWrapper' import { collectDataAttributes } from './lib/attributes' + +export default ReactVisual export { LazyVideo, VisualWrapper, collectDataAttributes } export { ReactVisualTypes, From 4053e7340edd7b30e008ed2ab39f0e0ba1d6aaf0 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 24 Oct 2023 22:21:21 -0700 Subject: [PATCH 03/29] Use image and video rendering from next-visual --- packages/react/src/ReactVisual.tsx | 60 +++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/react/src/ReactVisual.tsx b/packages/react/src/ReactVisual.tsx index 8410b3f..c11f215 100644 --- a/packages/react/src/ReactVisual.tsx +++ b/packages/react/src/ReactVisual.tsx @@ -1,11 +1,69 @@ import type { ReactElement } from 'react' +import VisualWrapper from './VisualWrapper' +import LazyVideo from './LazyVideo' +import { collectDataAttributes } from './lib/attributes' + import { ReactVisualTypes } from './types/reactVisualTypes' export default function ReactVisual( props: ReactVisualTypes ): ReactElement | null { + + // Destructure props + const { + image, + video, + placeholderData, + expand, + aspect, + width, + height, + fit = 'cover', + position, + priority, + sizes, + imageLoader, + paused, + alt, + className = '', + style = {}, + } = props + return ( -

Hey

+ + + {/* Render image */} + { image && } + + {/* Render video element */} + { video && } + + ) } From 27cc4cdf87fba289183a0f954fb41738f95f4876 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 25 Oct 2023 07:15:38 -0700 Subject: [PATCH 04/29] Fix type reference --- packages/next/src/index.ts | 1 + packages/sanity-next/src/index.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index b59cc4d..681f4f6 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -1,3 +1,4 @@ import NextVisual from './NextVisual' + export default NextVisual export { NextVisualProps } from './types/nextVisualTypes' diff --git a/packages/sanity-next/src/index.ts b/packages/sanity-next/src/index.ts index 656f9d2..1e023df 100644 --- a/packages/sanity-next/src/index.ts +++ b/packages/sanity-next/src/index.ts @@ -1,5 +1,6 @@ import SanityNextVisual from './SanityNextVisual' + export default SanityNextVisual export { SanityNextVisualProps } from './types/sanityNextVisualTypes' -export { ObjectFitOption } from '@react-visual/next' +export { ObjectFitOption } from '@react-visual/react' export { makeImageUrl, makeImageBuilder, makeFileUrl } from './lib/urlBuilding' From 23003855836af5e9917b4ced238d9749aa228cb0 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 25 Oct 2023 23:01:49 -0700 Subject: [PATCH 05/29] Implement picture rendering in child element --- packages/next/src/types/nextVisualTypes.ts | 4 ++- packages/react/src/PictureImage.tsx | 34 +++++++++++++++++++ packages/react/src/ReactVisual.tsx | 18 +++++----- packages/react/src/types/pictureImageTypes.ts | 12 +++++++ packages/react/src/types/reactVisualTypes.ts | 1 - 5 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 packages/react/src/PictureImage.tsx create mode 100644 packages/react/src/types/pictureImageTypes.ts diff --git a/packages/next/src/types/nextVisualTypes.ts b/packages/next/src/types/nextVisualTypes.ts index b6314bc..dd47b38 100644 --- a/packages/next/src/types/nextVisualTypes.ts +++ b/packages/next/src/types/nextVisualTypes.ts @@ -1,3 +1,5 @@ import type { ReactVisualTypes } from '@react-visual/react' -export type NextVisualProps = ReactVisualTypes +export type NextVisualProps = ReactVisualTypes & { + placeholderData?: string +} diff --git a/packages/react/src/PictureImage.tsx b/packages/react/src/PictureImage.tsx new file mode 100644 index 0000000..85d36a1 --- /dev/null +++ b/packages/react/src/PictureImage.tsx @@ -0,0 +1,34 @@ +import type { ReactElement } from 'react' +import type { pictureImageProps } from './types/pictureImageTypes' + +export default function PictureImage( + props: pictureImageProps +): ReactElement { + + // Destructure props + const { + src, + fit = 'cover', + position, + priority, + sizes, + imageLoader, + alt, + } = props + + // Apply layout styles + const style = { + objectFit: fit, + objectPosition: position + } + + // Unless priority flag was set, lazy load the image + const loading = priority ? undefined : 'lazy' + + // Always wrap in picture element for standard DOM structure + return ( + + + + ) +} diff --git a/packages/react/src/ReactVisual.tsx b/packages/react/src/ReactVisual.tsx index c11f215..3d1e8b6 100644 --- a/packages/react/src/ReactVisual.tsx +++ b/packages/react/src/ReactVisual.tsx @@ -2,8 +2,9 @@ import type { ReactElement } from 'react' import VisualWrapper from './VisualWrapper' import LazyVideo from './LazyVideo' -import { collectDataAttributes } from './lib/attributes' +import PictureImage from './PictureImage' +import { collectDataAttributes } from './lib/attributes' import { ReactVisualTypes } from './types/reactVisualTypes' export default function ReactVisual( @@ -14,7 +15,6 @@ export default function ReactVisual( const { image, video, - placeholderData, expand, aspect, width, @@ -30,6 +30,9 @@ export default function ReactVisual( style = {}, } = props + // If no asset, return nothing + if (!image && !video) return null + return ( {/* Render image */} - { image && } {/* Render video element */} diff --git a/packages/react/src/types/pictureImageTypes.ts b/packages/react/src/types/pictureImageTypes.ts new file mode 100644 index 0000000..c8c6abb --- /dev/null +++ b/packages/react/src/types/pictureImageTypes.ts @@ -0,0 +1,12 @@ +import { ReactVisualTypes } from './reactVisualTypes'; + +export type pictureImageProps = Pick & { + src: ReactVisualTypes['image'] +} diff --git a/packages/react/src/types/reactVisualTypes.ts b/packages/react/src/types/reactVisualTypes.ts index c267b7f..9033248 100644 --- a/packages/react/src/types/reactVisualTypes.ts +++ b/packages/react/src/types/reactVisualTypes.ts @@ -4,7 +4,6 @@ export type ReactVisualTypes = { image?: string video?: string - placeholderData?: string expand?: boolean aspect?: number // An explict aspect ratio From 865dc964cf5b4e040f2dcc2b8fe0cc103f5729cf Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 25 Oct 2023 23:58:00 -0700 Subject: [PATCH 06/29] Initial srcset rendering implementation --- .../cypress/component/ReactVisual.cy.tsx | 20 ++++++++--- packages/react/src/PictureImage.tsx | 34 +++++++++++++++++++ packages/react/src/types/pictureImageTypes.ts | 2 +- packages/react/src/types/reactVisualTypes.ts | 7 +++- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index 4877471..26ac2da 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -1,9 +1,5 @@ import ReactVisual from '../../src' -// Viewport sizes -const VW = Cypress.config('viewportWidth'), - VH = Cypress.config('viewportHeight') - describe('no asset', () => { it('renders nothing', () => { @@ -52,3 +48,19 @@ describe('fixed size', () => { cy.get('video').isPlaying() }) }) + +describe('sources', () => { + + it.only('renders simple srcset', () => { + cy.mount( { + const height = Math.round(width * 200 / 300) + return `https://placehold.co/${width}x${height}` + }} + aspect={300/200} + alt=''/>) + cy.get('img').hasDimensions(300, 200) + }) + +}) diff --git a/packages/react/src/PictureImage.tsx b/packages/react/src/PictureImage.tsx index 85d36a1..92e968d 100644 --- a/packages/react/src/PictureImage.tsx +++ b/packages/react/src/PictureImage.tsx @@ -1,5 +1,7 @@ import type { ReactElement } from 'react' import type { pictureImageProps } from './types/pictureImageTypes' +import type { ImageLoader } from './types/reactVisualTypes' +type ImageSrc = pictureImageProps['src'] export default function PictureImage( props: pictureImageProps @@ -28,7 +30,39 @@ export default function PictureImage( // Always wrap in picture element for standard DOM structure return ( + {imageLoader && } ) } + +// Make the array of Picture sources +function Sources({ src, imageLoader }: { + src: ImageSrc + imageLoader: ImageLoader +}): ReactElement { + + const widths = [400, 200] + + // Make the srcset + const srcSet = makeSrcSet(src, widths, imageLoader) + + // Make the source + return ( + + ) + +} + +// Make a srcset string from an array of width integers using the imageLoader +// function +function makeSrcSet( + src: ImageSrc, + widths: number[], + imageLoader: ImageLoader +): string { + return widths.map((width) => { + return imageLoader({ src, width }) + ` w${width}` + }).join(', ') + +} diff --git a/packages/react/src/types/pictureImageTypes.ts b/packages/react/src/types/pictureImageTypes.ts index c8c6abb..2c7883b 100644 --- a/packages/react/src/types/pictureImageTypes.ts +++ b/packages/react/src/types/pictureImageTypes.ts @@ -8,5 +8,5 @@ export type pictureImageProps = Pick & { - src: ReactVisualTypes['image'] + src: Required['image'] } diff --git a/packages/react/src/types/reactVisualTypes.ts b/packages/react/src/types/reactVisualTypes.ts index 9033248..90870d4 100644 --- a/packages/react/src/types/reactVisualTypes.ts +++ b/packages/react/src/types/reactVisualTypes.ts @@ -14,7 +14,7 @@ export type ReactVisualTypes = { priority?: boolean sizes?: string - imageLoader?: Function + imageLoader?: ImageLoader paused?: boolean @@ -24,6 +24,11 @@ export type ReactVisualTypes = { style?: CSSProperties } +export type ImageLoader = ({ src, width }: { + src: string + width: number +}) => string + export type ObjectFitOption = 'cover' | 'contain' // Deprecated From fd1cff07229881d4d9a77fd9f7691fc121f60891 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 11:05:51 -0800 Subject: [PATCH 07/29] Support width/height as numeric strings --- packages/react/src/VisualWrapper.tsx | 5 +++-- packages/react/src/lib/values.ts | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 packages/react/src/lib/values.ts diff --git a/packages/react/src/VisualWrapper.tsx b/packages/react/src/VisualWrapper.tsx index ce0657b..98326d2 100644 --- a/packages/react/src/VisualWrapper.tsx +++ b/packages/react/src/VisualWrapper.tsx @@ -1,5 +1,6 @@ import type { CSSProperties, ReactElement } from 'react' import { fillStyles } from './lib/styles' +import { isNumeric } from './lib/values' // Wraps media elements and applys layout and other functionality export default function VisualWrapper({ @@ -10,8 +11,8 @@ export default function VisualWrapper({ // apply width, height and aspect const layoutStyles = expand ? fillStyles : { position: 'relative', // For expanded elements - width: typeof width == 'number' ? `${width}px` : width, - height: typeof height == 'number' ? `${height}px` : height, + width: isNumeric(width) ? `${width}px` : width, + height: isNumeric(height) ? `${height}px` : height, aspectRatio: aspect, maxWidth: '100%', // Don't exceed container width } as CSSProperties diff --git a/packages/react/src/lib/values.ts b/packages/react/src/lib/values.ts new file mode 100644 index 0000000..8b9b50f --- /dev/null +++ b/packages/react/src/lib/values.ts @@ -0,0 +1,6 @@ + +// Test if a valye is numeric +export function isNumeric(value: any): Boolean { + if (!value) return false + return !!String(value).match(/^\d+$/) +} From 3719ef323ff073a4aa8bcf1e2558a579f4abacb6 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 11:06:15 -0800 Subject: [PATCH 08/29] Generate srcset using Next.js conventions --- packages/react/src/PictureImage.tsx | 5 ++--- packages/react/src/lib/sizes.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 packages/react/src/lib/sizes.ts diff --git a/packages/react/src/PictureImage.tsx b/packages/react/src/PictureImage.tsx index 92e968d..141970b 100644 --- a/packages/react/src/PictureImage.tsx +++ b/packages/react/src/PictureImage.tsx @@ -1,6 +1,7 @@ import type { ReactElement } from 'react' import type { pictureImageProps } from './types/pictureImageTypes' import type { ImageLoader } from './types/reactVisualTypes' +import { srcsetSizes } from './lib/sizes' type ImageSrc = pictureImageProps['src'] export default function PictureImage( @@ -42,10 +43,8 @@ function Sources({ src, imageLoader }: { imageLoader: ImageLoader }): ReactElement { - const widths = [400, 200] - // Make the srcset - const srcSet = makeSrcSet(src, widths, imageLoader) + const srcSet = makeSrcSet(src, srcsetSizes, imageLoader) // Make the source return ( diff --git a/packages/react/src/lib/sizes.ts b/packages/react/src/lib/sizes.ts new file mode 100644 index 0000000..5fc0ec1 --- /dev/null +++ b/packages/react/src/lib/sizes.ts @@ -0,0 +1,14 @@ + +// Based on +// https://nextjs.org/docs/pages/api-reference/components/image#devicesizes +const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840] + +// Based on +// https://nextjs.org/docs/pages/api-reference/components/image#imagesizes +const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384] + +// The sizes for which srcsets should be produced +export const srcsetSizes = [ + ...imageSizes, + ...deviceSizes, +] From 8b226d4584f828533a2b2824ebffa15e3eb79f95 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 12:40:48 -0800 Subject: [PATCH 09/29] Expand to wrapper --- packages/react/src/PictureImage.tsx | 8 ++++++-- packages/react/src/ReactVisual.tsx | 4 ++++ packages/react/src/types/pictureImageTypes.ts | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/react/src/PictureImage.tsx b/packages/react/src/PictureImage.tsx index 141970b..465e29b 100644 --- a/packages/react/src/PictureImage.tsx +++ b/packages/react/src/PictureImage.tsx @@ -16,11 +16,12 @@ export default function PictureImage( priority, sizes, imageLoader, + style, alt, } = props // Apply layout styles - const style = { + const layoutStyles = { objectFit: fit, objectPosition: position } @@ -32,7 +33,10 @@ export default function PictureImage( return ( {imageLoader && } - + ) } diff --git a/packages/react/src/ReactVisual.tsx b/packages/react/src/ReactVisual.tsx index 3d1e8b6..23c181d 100644 --- a/packages/react/src/ReactVisual.tsx +++ b/packages/react/src/ReactVisual.tsx @@ -53,6 +53,10 @@ export default function ReactVisual( position, priority, imageLoader, + style: { // Expand to wrapper when wrapper has layout + width: expand || width || aspect ? '100%': undefined, + height: expand || height ? '100%' : undefined, + } }} /> } {/* Render video element */} diff --git a/packages/react/src/types/pictureImageTypes.ts b/packages/react/src/types/pictureImageTypes.ts index 2c7883b..d5e3c6f 100644 --- a/packages/react/src/types/pictureImageTypes.ts +++ b/packages/react/src/types/pictureImageTypes.ts @@ -6,7 +6,8 @@ export type pictureImageProps = Pick & { src: Required['image'] } From a0353eb9912a52a832f10950a7ca1b8006e4c452 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 12:41:02 -0800 Subject: [PATCH 10/29] Fix srcset syntax --- packages/react/cypress/component/ReactVisual.cy.tsx | 9 +++++++-- packages/react/src/PictureImage.tsx | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index 26ac2da..d1a6afc 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -51,7 +51,9 @@ describe('fixed size', () => { describe('sources', () => { - it.only('renders simple srcset', () => { + it.only('renders srset with no sizes prop', () => { + + // Load the image cy.mount( { @@ -60,7 +62,10 @@ describe('sources', () => { }} aspect={300/200} alt=''/>) - cy.get('img').hasDimensions(300, 200) + + // Get one of the sizes that should be been rendered + cy.get('source').invoke('attr', 'srcset') + .should('contain', 'https://placehold.co/384x256 384w') }) }) diff --git a/packages/react/src/PictureImage.tsx b/packages/react/src/PictureImage.tsx index 465e29b..9ea629d 100644 --- a/packages/react/src/PictureImage.tsx +++ b/packages/react/src/PictureImage.tsx @@ -65,7 +65,7 @@ function makeSrcSet( imageLoader: ImageLoader ): string { return widths.map((width) => { - return imageLoader({ src, width }) + ` w${width}` + return imageLoader({ src, width }) + ` ${width}w` }).join(', ') } From 9736316d97e8ab2d00967f405fb2d4a2634b9c7e Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 13:29:09 -0800 Subject: [PATCH 11/29] Rename PictureImageProps --- packages/react/src/PictureImage.tsx | 4 ++-- packages/react/src/types/pictureImageTypes.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/PictureImage.tsx b/packages/react/src/PictureImage.tsx index 9ea629d..ad84e76 100644 --- a/packages/react/src/PictureImage.tsx +++ b/packages/react/src/PictureImage.tsx @@ -1,11 +1,11 @@ import type { ReactElement } from 'react' -import type { pictureImageProps } from './types/pictureImageTypes' +import type { PictureImageProps } from './types/pictureImageTypes' import type { ImageLoader } from './types/reactVisualTypes' import { srcsetSizes } from './lib/sizes' type ImageSrc = pictureImageProps['src'] export default function PictureImage( - props: pictureImageProps + props: PictureImageProps ): ReactElement { // Destructure props diff --git a/packages/react/src/types/pictureImageTypes.ts b/packages/react/src/types/pictureImageTypes.ts index d5e3c6f..b0d9076 100644 --- a/packages/react/src/types/pictureImageTypes.ts +++ b/packages/react/src/types/pictureImageTypes.ts @@ -1,6 +1,6 @@ import { ReactVisualTypes } from './reactVisualTypes'; -export type pictureImageProps = Pick Date: Thu, 16 Nov 2023 13:29:36 -0800 Subject: [PATCH 12/29] Test of that correct image is loaded --- .../cypress/component/ReactVisual.cy.tsx | 56 +++++++++++++++++-- packages/react/src/PictureImage.tsx | 36 ++++++++---- packages/react/src/lib/sizes.ts | 14 ++--- 3 files changed, 83 insertions(+), 23 deletions(-) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index d1a6afc..f61be0c 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -51,9 +51,8 @@ describe('fixed size', () => { describe('sources', () => { - it.only('renders srset with no sizes prop', () => { + it('renders srset with no sizes prop', () => { - // Load the image cy.mount( { @@ -64,8 +63,57 @@ describe('sources', () => { alt=''/>) // Get one of the sizes that should be been rendered - cy.get('source').invoke('attr', 'srcset') - .should('contain', 'https://placehold.co/384x256 384w') + cy.get('[srcset]').invoke('attr', 'srcset') + .should('contain', '640x427 640w') + + // Only be included when `sizes` specified + .should('not.contain', ' 16w') + }) + + it('doesn\'t use imageSizes when sizes == 100vw', () => { + + cy.mount( { + const height = Math.round(width * 200 / 300) + return `https://placehold.co/${width}x${height}` + }} + aspect={300/200} + sizes='100vw' + alt=''/>) + + cy.get('[srcset]').invoke('attr', 'srcset') + .should('not.contain', ' 16w') + }) + + it('it adds narrower widths with sizes prop', () => { + + // Clear the browser cache + cy.wrap(Cypress.automation('remote:debugger:protocol', { + command: 'Network.clearBrowserCache', + })) + + // This is the particular image we expect to load + cy.intercept('https://placehold.co/256x171').as('50vw image') + + cy.mount( { + const height = Math.round(width * 200 / 300) + return `https://placehold.co/${width}x${height}` + }} + aspect={300/200} + width='50%' + sizes='50vw' + alt=''/>) + + // The image that should have been loaded + cy.get('[srcset]').invoke('attr', 'srcset') + .should('contain', 'https://placehold.co/256x171 256w') + + // Ensure that we didn't load too big or small of an image + cy.wait('@50vw image') + }) }) diff --git a/packages/react/src/PictureImage.tsx b/packages/react/src/PictureImage.tsx index ad84e76..ba05988 100644 --- a/packages/react/src/PictureImage.tsx +++ b/packages/react/src/PictureImage.tsx @@ -1,8 +1,11 @@ import type { ReactElement } from 'react' import type { PictureImageProps } from './types/pictureImageTypes' import type { ImageLoader } from './types/reactVisualTypes' -import { srcsetSizes } from './lib/sizes' -type ImageSrc = pictureImageProps['src'] +import { deviceSizes, imageSizes } from './lib/sizes' + +type ImageSrc = PictureImageProps['src'] +type SourcesProps = Pick & + Required> export default function PictureImage( props: PictureImageProps @@ -29,32 +32,45 @@ export default function PictureImage( // Unless priority flag was set, lazy load the image const loading = priority ? undefined : 'lazy' + // Determine the widths to use for srcset. Include small widths only when + // sizes is specified, like Next.js does + const srcsetWidths = [ + ...(sizes && sizes != '100vw' ? imageSizes : []), + ...deviceSizes, + ] + + // Make the img srcset. When I had a single with no type or media + // attribute, the srcset would not affect the image loaded. Thus, I'm + // applying it to the img tag + const srcSet = imageLoader && makeSrcSet(src, srcsetWidths, imageLoader) + // Always wrap in picture element for standard DOM structure return ( - {imageLoader && } ) } // Make the array of Picture sources -function Sources({ src, imageLoader }: { - src: ImageSrc - imageLoader: ImageLoader -}): ReactElement { +function Sources({ src, sizes, imageLoader }: SourcesProps): ReactElement { + + // Include small widths only when sizes is specified, like Next.js does + const widths = [ + ...(sizes && sizes != '100vw' ? imageSizes : []), + ...deviceSizes, + ] // Make the srcset - const srcSet = makeSrcSet(src, srcsetSizes, imageLoader) + const srcSet = makeSrcSet(src, widths, imageLoader) // Make the source return ( ) - } // Make a srcset string from an array of width integers using the imageLoader diff --git a/packages/react/src/lib/sizes.ts b/packages/react/src/lib/sizes.ts index 5fc0ec1..c088e4d 100644 --- a/packages/react/src/lib/sizes.ts +++ b/packages/react/src/lib/sizes.ts @@ -1,14 +1,10 @@ // Based on -// https://nextjs.org/docs/pages/api-reference/components/image#devicesizes -const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840] +// https://nextjs.org/docs/pages/api-reference/components/image#imagesizes +export const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384] + // Based on -// https://nextjs.org/docs/pages/api-reference/components/image#imagesizes -const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384] +// https://nextjs.org/docs/pages/api-reference/components/image#devicesizes +export const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840] -// The sizes for which srcsets should be produced -export const srcsetSizes = [ - ...imageSizes, - ...deviceSizes, -] From 95d4ebb69048823a0e4fd4119ad923f672a52857 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 15:13:40 -0800 Subject: [PATCH 13/29] Rename ReactVisualProps --- packages/next/src/types/nextVisualTypes.ts | 4 ++-- packages/react/src/ReactVisual.tsx | 4 ++-- packages/react/src/index.ts | 2 +- packages/react/src/types/pictureImageTypes.ts | 6 +++--- packages/react/src/types/reactVisualTypes.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/next/src/types/nextVisualTypes.ts b/packages/next/src/types/nextVisualTypes.ts index dd47b38..45c4663 100644 --- a/packages/next/src/types/nextVisualTypes.ts +++ b/packages/next/src/types/nextVisualTypes.ts @@ -1,5 +1,5 @@ -import type { ReactVisualTypes } from '@react-visual/react' +import type { ReactVisualProps } from '@react-visual/react' -export type NextVisualProps = ReactVisualTypes & { +export type NextVisualProps = ReactVisualProps & { placeholderData?: string } diff --git a/packages/react/src/ReactVisual.tsx b/packages/react/src/ReactVisual.tsx index 23c181d..7038546 100644 --- a/packages/react/src/ReactVisual.tsx +++ b/packages/react/src/ReactVisual.tsx @@ -5,10 +5,10 @@ import LazyVideo from './LazyVideo' import PictureImage from './PictureImage' import { collectDataAttributes } from './lib/attributes' -import { ReactVisualTypes } from './types/reactVisualTypes' +import { ReactVisualProps } from './types/reactVisualTypes' export default function ReactVisual( - props: ReactVisualTypes + props: ReactVisualProps ): ReactElement | null { // Destructure props diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 12a1ab9..9b03302 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,7 +6,7 @@ import { collectDataAttributes } from './lib/attributes' export default ReactVisual export { LazyVideo, VisualWrapper, collectDataAttributes } export { - ReactVisualTypes, + ReactVisualProps, ObjectFit, ObjectFitOption } from './types/reactVisualTypes' diff --git a/packages/react/src/types/pictureImageTypes.ts b/packages/react/src/types/pictureImageTypes.ts index b0d9076..8b41dc9 100644 --- a/packages/react/src/types/pictureImageTypes.ts +++ b/packages/react/src/types/pictureImageTypes.ts @@ -1,6 +1,6 @@ -import { ReactVisualTypes } from './reactVisualTypes'; +import { ReactVisualProps } from './reactVisualTypes'; -export type PictureImageProps = Pick & { - src: Required['image'] + src: Required['image'] } diff --git a/packages/react/src/types/reactVisualTypes.ts b/packages/react/src/types/reactVisualTypes.ts index 90870d4..0851f36 100644 --- a/packages/react/src/types/reactVisualTypes.ts +++ b/packages/react/src/types/reactVisualTypes.ts @@ -1,6 +1,6 @@ import type { CSSProperties } from 'react' -export type ReactVisualTypes = { +export type ReactVisualProps = { image?: string video?: string From 3346debf1d6659a9ca4fd369ca3acc7676044b0b Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 15:14:02 -0800 Subject: [PATCH 14/29] Support rendering source tags from types and media --- .../cypress/component/ReactVisual.cy.tsx | 78 ++++++++++++++--- packages/react/cypress/support/commands.ts | 10 +++ packages/react/src/PictureImage.tsx | 84 ++++++++++++++----- packages/react/src/ReactVisual.tsx | 4 + packages/react/src/types/pictureImageTypes.ts | 2 + packages/react/src/types/reactVisualTypes.ts | 11 ++- 6 files changed, 153 insertions(+), 36 deletions(-) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index f61be0c..5e73cb0 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -49,7 +49,7 @@ describe('fixed size', () => { }) }) -describe('sources', () => { +describe('srcset', () => { it('renders srset with no sizes prop', () => { @@ -88,31 +88,83 @@ describe('sources', () => { it('it adds narrower widths with sizes prop', () => { - // Clear the browser cache - cy.wrap(Cypress.automation('remote:debugger:protocol', { - command: 'Network.clearBrowserCache', - })) - // This is the particular image we expect to load - cy.intercept('https://placehold.co/256x171').as('50vw image') + cy.clearCache() + cy.intercept('https://placehold.co/256x256').as('50vw') cy.mount( { - const height = Math.round(width * 200 / 300) - return `https://placehold.co/${width}x${height}` + return `https://placehold.co/${width}x${width}` }} - aspect={300/200} + aspect={1} width='50%' sizes='50vw' alt=''/>) // The image that should have been loaded cy.get('[srcset]').invoke('attr', 'srcset') - .should('contain', 'https://placehold.co/256x171 256w') + .should('contain', 'https://placehold.co/256x256 256w') // Ensure that we didn't load too big or small of an image - cy.wait('@50vw image') + cy.wait('@50vw') + + }) + +}) + +describe('sources', () => { + + it('supports rendering sources for mimetypes', () => { + + cy.clearCache() + cy.intercept('https://placehold.co/640x640.webp').as('webp') + + cy.mount( { + const ext = type?.includes('webp') ? '.webp' : '' + return `https://placehold.co/${width}x${width}${ext}` + }} + aspect={1} + alt=''/>) + + cy.wait('@webp') + + }) + + it('supports rendering sources for mimetypes and media queries', () => { + + cy.clearCache() + cy.intercept('https://placehold.co/640x320.webp').as('landscape') + cy.intercept('https://placehold.co/640x640.webp').as('portrait') + + // Start at a landscape viewport + cy.viewport(500, 400) + + cy.mount( { + + // Use a narrower aspect on landscape and a square on mobile + const height = media?.includes('landscape') ? + width * 0.5 : width + + const ext = type?.includes('webp') ? '.webp' : '' + return `https://placehold.co/${width}x${height}${ext}` + }} + width='100%' + alt=''/>) + + // Landscape should have loaded + cy.wait('@landscape') + + // Switch to portrait, which should load the other source + cy.viewport(500, 600) + cy.wait('@portrait') }) diff --git a/packages/react/cypress/support/commands.ts b/packages/react/cypress/support/commands.ts index 6defc28..173af45 100644 --- a/packages/react/cypress/support/commands.ts +++ b/packages/react/cypress/support/commands.ts @@ -26,6 +26,14 @@ Cypress.Commands.add('isPaused', cy.wrap(subject).should('have.prop', 'paused', true) }) +// Clear browser cache +// https://stackoverflow.com/a/72945339/59160 +Cypress.Commands.add('clearCache', () => { + cy.wrap(Cypress.automation('remote:debugger:protocol', { + command: 'Network.clearBrowserCache', + })) +}) + // Add Typescript support for custom commaands // https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands export {}; @@ -40,6 +48,8 @@ declare global { isPlaying(): Chainable isPaused(): Chainable + + clearCache(): Chainable } } } diff --git a/packages/react/src/PictureImage.tsx b/packages/react/src/PictureImage.tsx index ba05988..ed7a385 100644 --- a/packages/react/src/PictureImage.tsx +++ b/packages/react/src/PictureImage.tsx @@ -1,11 +1,16 @@ import type { ReactElement } from 'react' import type { PictureImageProps } from './types/pictureImageTypes' -import type { ImageLoader } from './types/reactVisualTypes' +import type { ImageLoader, SourceMedia, SourceType } from './types/reactVisualTypes' import { deviceSizes, imageSizes } from './lib/sizes' type ImageSrc = PictureImageProps['src'] -type SourcesProps = Pick & - Required> +type SourcesProps = { + widths: number[] + imageLoader: Required['imageLoader'] + src: ImageSrc + type?: SourceType + media?: SourceMedia +} export default function PictureImage( props: PictureImageProps @@ -19,6 +24,8 @@ export default function PictureImage( priority, sizes, imageLoader, + sourceTypes, + sourceMedia, style, alt, } = props @@ -42,11 +49,25 @@ export default function PictureImage( // Make the img srcset. When I had a single with no type or media // attribute, the srcset would not affect the image loaded. Thus, I'm // applying it to the img tag - const srcSet = imageLoader && makeSrcSet(src, srcsetWidths, imageLoader) + const srcSet = imageLoader && makeSrcSet(srcsetWidths, imageLoader, { src }) + + // Additional sources to create + const sourceVariants = makeSourceVariants(sourceTypes, sourceMedia) // Always wrap in picture element for standard DOM structure return ( + + {/* Make s */} + {imageLoader && sourceVariants?.map(({ type, media, key }) => ( + + ))} + + {/* The main */} + ) } // Make a srcset string from an array of width integers using the imageLoader -// function +// function. Params such as src, type, and media are passed to the loader +// function to customize the image returned. function makeSrcSet( - src: ImageSrc, widths: number[], - imageLoader: ImageLoader + imageLoader: ImageLoader, + params: { + src: ImageSrc + type?: SourceType + media?: SourceMedia + } ): string { return widths.map((width) => { - return imageLoader({ src, width }) + ` ${width}w` + return imageLoader({ width, ...params }) + ` ${width}w` }).join(', ') - } diff --git a/packages/react/src/ReactVisual.tsx b/packages/react/src/ReactVisual.tsx index 7038546..6834e90 100644 --- a/packages/react/src/ReactVisual.tsx +++ b/packages/react/src/ReactVisual.tsx @@ -24,6 +24,8 @@ export default function ReactVisual( priority, sizes, imageLoader, + sourceTypes, + sourceMedia, paused, alt, className = '', @@ -53,6 +55,8 @@ export default function ReactVisual( position, priority, imageLoader, + sourceTypes, + sourceMedia, style: { // Expand to wrapper when wrapper has layout width: expand || width || aspect ? '100%': undefined, height: expand || height ? '100%' : undefined, diff --git a/packages/react/src/types/pictureImageTypes.ts b/packages/react/src/types/pictureImageTypes.ts index 8b41dc9..b1be789 100644 --- a/packages/react/src/types/pictureImageTypes.ts +++ b/packages/react/src/types/pictureImageTypes.ts @@ -7,6 +7,8 @@ export type PictureImageProps = Pick & { src: Required['image'] diff --git a/packages/react/src/types/reactVisualTypes.ts b/packages/react/src/types/reactVisualTypes.ts index 0851f36..5c19185 100644 --- a/packages/react/src/types/reactVisualTypes.ts +++ b/packages/react/src/types/reactVisualTypes.ts @@ -15,6 +15,8 @@ export type ReactVisualProps = { priority?: boolean sizes?: string imageLoader?: ImageLoader + sourceTypes?: SourceType[] + sourceMedia?: SourceMedia[] paused?: boolean @@ -24,13 +26,20 @@ export type ReactVisualProps = { style?: CSSProperties } -export type ImageLoader = ({ src, width }: { +export type ImageLoader = ({ src, width, type, media }: { src: string width: number + type?: SourceType + media?: SourceMedia }) => string export type ObjectFitOption = 'cover' | 'contain' +export type SourceType = 'image/jpeg' | 'image/png' | 'image/gif' | + 'image/avif' | 'image/webp' | string + +export type SourceMedia = 'orientation:landscape' | 'orientation:portrait' | string + // Deprecated export enum ObjectFit { Cover = 'cover', From fa07da4a117e77ea88064c635308fedeacd8c464 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 15:38:30 -0800 Subject: [PATCH 15/29] Add tests of VisualWrapper --- .../cypress/component/VisualWrapper.cy.tsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/react/cypress/component/VisualWrapper.cy.tsx diff --git a/packages/react/cypress/component/VisualWrapper.cy.tsx b/packages/react/cypress/component/VisualWrapper.cy.tsx new file mode 100644 index 0000000..671e4bd --- /dev/null +++ b/packages/react/cypress/component/VisualWrapper.cy.tsx @@ -0,0 +1,57 @@ +import { VisualWrapper } from '../../src' + +// Shared props +const sharedProps = { + style: { background: 'black', color: 'white' }, + className: 'wrapper', +} +const style = { background: 'black' } + +// Viewport sizes +const VW = Cypress.config('viewportWidth'), + VH = Cypress.config('viewportHeight') + +describe('fixed size', () => { + + it('integer values', () => { + cy.mount() + cy.get('.wrapper').hasDimensions(300, 200) + }) + + it('string values', () => { + cy.mount() + cy.get('.wrapper').hasDimensions(300, 200) + }) + + it('percentage values', () => { + cy.mount() + cy.get('.wrapper').hasDimensions(VW, VH / 2) + }) + +}) + +it('expands', () => { + cy.mount() + cy.get('.wrapper').hasDimensions(VW, VH) +}) + +it('supports aspect', () => { + cy.mount() + cy.get('.wrapper').hasDimensions(VW, VH / 2) +}) + +it('supports children', () => { + cy.mount( +

Hey

+
) + cy.get('h1').contains('Hey') +}) + + + From c4231a26bbf8fc2ad68d52b0fec232b26ea5afaa Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 15:39:01 -0800 Subject: [PATCH 16/29] Wait or assets to load --- .../react/cypress/component/ReactVisual.cy.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index 5e73cb0..540212f 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -1,5 +1,10 @@ import ReactVisual from '../../src' +beforeEach(() => { + cy.clearCache() + cy.intercept({ hostname: 'placehold.co' }).as('asset') +}) + describe('no asset', () => { it('renders nothing', () => { @@ -21,6 +26,7 @@ describe('fixed size', () => { width={300} height={200} alt=''/>) + cy.wait('@asset') cy.get('img').hasDimensions(300, 200) }) @@ -30,6 +36,7 @@ describe('fixed size', () => { width={300} height={200} alt=''/>) + cy.wait('@asset') cy.get('video').hasDimensions(300, 200) cy.get('video').isPlaying() }) @@ -42,6 +49,7 @@ describe('fixed size', () => { height={200} alt='' data-cy='next-visual' />) + cy.wait('@asset') cy.get('[data-cy=next-visual]').hasDimensions(300, 200) cy.get('img').hasDimensions(300, 200) cy.get('video').hasDimensions(300, 200) @@ -61,6 +69,7 @@ describe('srcset', () => { }} aspect={300/200} alt=''/>) + cy.wait('@asset') // Get one of the sizes that should be been rendered cy.get('[srcset]').invoke('attr', 'srcset') @@ -81,6 +90,7 @@ describe('srcset', () => { aspect={300/200} sizes='100vw' alt=''/>) + cy.wait('@asset') cy.get('[srcset]').invoke('attr', 'srcset') .should('not.contain', ' 16w') @@ -89,7 +99,6 @@ describe('srcset', () => { it('it adds narrower widths with sizes prop', () => { // This is the particular image we expect to load - cy.clearCache() cy.intercept('https://placehold.co/256x256').as('50vw') cy.mount( { width='50%' sizes='50vw' alt=''/>) + cy.wait('@asset') // The image that should have been loaded cy.get('[srcset]').invoke('attr', 'srcset') @@ -117,7 +127,6 @@ describe('sources', () => { it('supports rendering sources for mimetypes', () => { - cy.clearCache() cy.intercept('https://placehold.co/640x640.webp').as('webp') cy.mount( { }} aspect={1} alt=''/>) + cy.wait('@asset') cy.wait('@webp') @@ -136,7 +146,6 @@ describe('sources', () => { it('supports rendering sources for mimetypes and media queries', () => { - cy.clearCache() cy.intercept('https://placehold.co/640x320.webp').as('landscape') cy.intercept('https://placehold.co/640x640.webp').as('portrait') @@ -158,6 +167,7 @@ describe('sources', () => { }} width='100%' alt=''/>) + cy.wait('@asset') // Landscape should have loaded cy.wait('@landscape') From 33cd2436e6c26ceefe0c88e7d61c4e6ca0a4c7aa Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 15:39:12 -0800 Subject: [PATCH 17/29] Add tests of natural image size --- packages/react/cypress/component/ReactVisual.cy.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index 540212f..e5efd8c 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -57,6 +57,18 @@ describe('fixed size', () => { }) }) +describe('natural size', () => { + + it('renders image', () => { + cy.mount() + cy.wait('@asset') + cy.get('img').hasDimensions(200, 200) + }) + +}) + describe('srcset', () => { it('renders srset with no sizes prop', () => { From a4067966952caeb36f27c95971c4ee47c004024b Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 15:39:27 -0800 Subject: [PATCH 18/29] Test that the wrapper dimensions override natural image size --- packages/react/cypress/component/ReactVisual.cy.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index e5efd8c..49b34bf 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -22,7 +22,7 @@ describe('fixed size', () => { it('renders image', () => { cy.mount() @@ -32,7 +32,7 @@ describe('fixed size', () => { it('renders video', () => { cy.mount() @@ -43,8 +43,8 @@ describe('fixed size', () => { it('renders image & video', () => { cy.mount( Date: Thu, 16 Nov 2023 15:57:28 -0800 Subject: [PATCH 19/29] Move commands to a shared location Had to convert to JS to silence Cyrpess TS errors --- .../commands.ts => lib/cypress/commands.js | 24 ------------ packages/next/cypress/support/commands.ts | 38 ------------------- packages/next/cypress/support/component.ts | 2 +- packages/react/cypress/support/component.ts | 2 +- .../sanity-next/cypress/support/commands.ts | 38 ------------------- .../sanity-next/cypress/support/component.ts | 2 +- 6 files changed, 3 insertions(+), 103 deletions(-) rename packages/react/cypress/support/commands.ts => lib/cypress/commands.js (65%) delete mode 100644 packages/next/cypress/support/commands.ts delete mode 100644 packages/sanity-next/cypress/support/commands.ts diff --git a/packages/react/cypress/support/commands.ts b/lib/cypress/commands.js similarity index 65% rename from packages/react/cypress/support/commands.ts rename to lib/cypress/commands.js index 173af45..17fcf0b 100644 --- a/packages/react/cypress/support/commands.ts +++ b/lib/cypress/commands.js @@ -1,5 +1,3 @@ -/// - // Asset that el has dimensions Cypress.Commands.add('hasDimensions', { prevSubject: true }, @@ -33,25 +31,3 @@ Cypress.Commands.add('clearCache', () => { command: 'Network.clearBrowserCache', })) }) - -// Add Typescript support for custom commaands -// https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands -export {}; -declare global { - namespace Cypress { - interface Chainable { - - hasDimensions( - width: number, - height: number - ): Chainable - - isPlaying(): Chainable - isPaused(): Chainable - - clearCache(): Chainable - } - } -} - - diff --git a/packages/next/cypress/support/commands.ts b/packages/next/cypress/support/commands.ts deleted file mode 100644 index 012519e..0000000 --- a/packages/next/cypress/support/commands.ts +++ /dev/null @@ -1,38 +0,0 @@ -/// - -// Asset that el has dimensions -Cypress.Commands.add('hasDimensions', - { prevSubject: true }, - (subject, width, height) => { - - expect(subject.width()).to.equal(width) - expect(subject.height()).to.equal(height) - return subject -}) - -// Check that a video is playing -// https://glebbahmutov.com/blog/test-video-play/ -Cypress.Commands.add('isPlaying', - { prevSubject: true }, - (subject) => { - cy.wrap(subject).should('have.prop', 'paused', false) -}) - -// Add Typescript support for custom commaands -// https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands -export {}; -declare global { - namespace Cypress { - interface Chainable { - - hasDimensions( - width: number, - height: number - ): Chainable - - isPlaying(): Chainable - } - } -} - - diff --git a/packages/next/cypress/support/component.ts b/packages/next/cypress/support/component.ts index 41b935d..176475b 100644 --- a/packages/next/cypress/support/component.ts +++ b/packages/next/cypress/support/component.ts @@ -1,5 +1,5 @@ // Import commands.js using ES2015 syntax: -import './commands' +import '../../../../lib/cypress/commands' // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/packages/react/cypress/support/component.ts b/packages/react/cypress/support/component.ts index 41b935d..176475b 100644 --- a/packages/react/cypress/support/component.ts +++ b/packages/react/cypress/support/component.ts @@ -1,5 +1,5 @@ // Import commands.js using ES2015 syntax: -import './commands' +import '../../../../lib/cypress/commands' // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/packages/sanity-next/cypress/support/commands.ts b/packages/sanity-next/cypress/support/commands.ts deleted file mode 100644 index 0b5b878..0000000 --- a/packages/sanity-next/cypress/support/commands.ts +++ /dev/null @@ -1,38 +0,0 @@ -/// - -// Asset that el has dimensions -Cypress.Commands.add('hasDimensions', - { prevSubject: true }, - (subject, width, height) => { - - expect(subject.width()).to.equal(width) - expect(subject.height()).to.equal(height) - return subject -}) - -// Check that a video is paused -// https://glebbahmutov.com/blog/test-video-play/ -Cypress.Commands.add('isPlaying', - { prevSubject: true }, - (subject) => { - cy.wrap(subject).should('have.prop', 'paused', false) -}) - -// Add Typescript support for custom commaands -// https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands -export {}; -declare global { - namespace Cypress { - interface Chainable { - - hasDimensions( - width: number, - height: number - ): Chainable - - isPlaying(): Chainable - } - } -} - - diff --git a/packages/sanity-next/cypress/support/component.ts b/packages/sanity-next/cypress/support/component.ts index 41b935d..176475b 100644 --- a/packages/sanity-next/cypress/support/component.ts +++ b/packages/sanity-next/cypress/support/component.ts @@ -1,5 +1,5 @@ // Import commands.js using ES2015 syntax: -import './commands' +import '../../../../lib/cypress/commands' // Alternatively you can use CommonJS syntax: // require('./commands') From eed7b9055325b3b043e0b28046ab2095d87c8a24 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 16:00:15 -0800 Subject: [PATCH 20/29] Revert "Move commands to a shared location" This reverts commit a005fc1d4f6cf9b8075b04338f5b71280ac0015a. --- packages/next/cypress/support/commands.ts | 38 +++++++++++++++++++ packages/next/cypress/support/component.ts | 2 +- .../react/cypress/support/commands.ts | 24 ++++++++++++ packages/react/cypress/support/component.ts | 2 +- .../sanity-next/cypress/support/commands.ts | 38 +++++++++++++++++++ .../sanity-next/cypress/support/component.ts | 2 +- 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 packages/next/cypress/support/commands.ts rename lib/cypress/commands.js => packages/react/cypress/support/commands.ts (65%) create mode 100644 packages/sanity-next/cypress/support/commands.ts diff --git a/packages/next/cypress/support/commands.ts b/packages/next/cypress/support/commands.ts new file mode 100644 index 0000000..012519e --- /dev/null +++ b/packages/next/cypress/support/commands.ts @@ -0,0 +1,38 @@ +/// + +// Asset that el has dimensions +Cypress.Commands.add('hasDimensions', + { prevSubject: true }, + (subject, width, height) => { + + expect(subject.width()).to.equal(width) + expect(subject.height()).to.equal(height) + return subject +}) + +// Check that a video is playing +// https://glebbahmutov.com/blog/test-video-play/ +Cypress.Commands.add('isPlaying', + { prevSubject: true }, + (subject) => { + cy.wrap(subject).should('have.prop', 'paused', false) +}) + +// Add Typescript support for custom commaands +// https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands +export {}; +declare global { + namespace Cypress { + interface Chainable { + + hasDimensions( + width: number, + height: number + ): Chainable + + isPlaying(): Chainable + } + } +} + + diff --git a/packages/next/cypress/support/component.ts b/packages/next/cypress/support/component.ts index 176475b..41b935d 100644 --- a/packages/next/cypress/support/component.ts +++ b/packages/next/cypress/support/component.ts @@ -1,5 +1,5 @@ // Import commands.js using ES2015 syntax: -import '../../../../lib/cypress/commands' +import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/lib/cypress/commands.js b/packages/react/cypress/support/commands.ts similarity index 65% rename from lib/cypress/commands.js rename to packages/react/cypress/support/commands.ts index 17fcf0b..173af45 100644 --- a/lib/cypress/commands.js +++ b/packages/react/cypress/support/commands.ts @@ -1,3 +1,5 @@ +/// + // Asset that el has dimensions Cypress.Commands.add('hasDimensions', { prevSubject: true }, @@ -31,3 +33,25 @@ Cypress.Commands.add('clearCache', () => { command: 'Network.clearBrowserCache', })) }) + +// Add Typescript support for custom commaands +// https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands +export {}; +declare global { + namespace Cypress { + interface Chainable { + + hasDimensions( + width: number, + height: number + ): Chainable + + isPlaying(): Chainable + isPaused(): Chainable + + clearCache(): Chainable + } + } +} + + diff --git a/packages/react/cypress/support/component.ts b/packages/react/cypress/support/component.ts index 176475b..41b935d 100644 --- a/packages/react/cypress/support/component.ts +++ b/packages/react/cypress/support/component.ts @@ -1,5 +1,5 @@ // Import commands.js using ES2015 syntax: -import '../../../../lib/cypress/commands' +import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/packages/sanity-next/cypress/support/commands.ts b/packages/sanity-next/cypress/support/commands.ts new file mode 100644 index 0000000..0b5b878 --- /dev/null +++ b/packages/sanity-next/cypress/support/commands.ts @@ -0,0 +1,38 @@ +/// + +// Asset that el has dimensions +Cypress.Commands.add('hasDimensions', + { prevSubject: true }, + (subject, width, height) => { + + expect(subject.width()).to.equal(width) + expect(subject.height()).to.equal(height) + return subject +}) + +// Check that a video is paused +// https://glebbahmutov.com/blog/test-video-play/ +Cypress.Commands.add('isPlaying', + { prevSubject: true }, + (subject) => { + cy.wrap(subject).should('have.prop', 'paused', false) +}) + +// Add Typescript support for custom commaands +// https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands +export {}; +declare global { + namespace Cypress { + interface Chainable { + + hasDimensions( + width: number, + height: number + ): Chainable + + isPlaying(): Chainable + } + } +} + + diff --git a/packages/sanity-next/cypress/support/component.ts b/packages/sanity-next/cypress/support/component.ts index 176475b..41b935d 100644 --- a/packages/sanity-next/cypress/support/component.ts +++ b/packages/sanity-next/cypress/support/component.ts @@ -1,5 +1,5 @@ // Import commands.js using ES2015 syntax: -import '../../../../lib/cypress/commands' +import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') From c9414371d78b671d960c5e50e0272d669ce33f06 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 16:42:44 -0800 Subject: [PATCH 21/29] Drop network intercepting strategy It was unstable --- .../cypress/component/ReactVisual.cy.tsx | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index 49b34bf..20b7e4f 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -2,7 +2,6 @@ import ReactVisual from '../../src' beforeEach(() => { cy.clearCache() - cy.intercept({ hostname: 'placehold.co' }).as('asset') }) describe('no asset', () => { @@ -26,7 +25,6 @@ describe('fixed size', () => { width={300} height={200} alt=''/>) - cy.wait('@asset') cy.get('img').hasDimensions(300, 200) }) @@ -36,7 +34,6 @@ describe('fixed size', () => { width={300} height={200} alt=''/>) - cy.wait('@asset') cy.get('video').hasDimensions(300, 200) cy.get('video').isPlaying() }) @@ -49,7 +46,6 @@ describe('fixed size', () => { height={200} alt='' data-cy='next-visual' />) - cy.wait('@asset') cy.get('[data-cy=next-visual]').hasDimensions(300, 200) cy.get('img').hasDimensions(300, 200) cy.get('video').hasDimensions(300, 200) @@ -63,7 +59,6 @@ describe('natural size', () => { cy.mount() - cy.wait('@asset') cy.get('img').hasDimensions(200, 200) }) @@ -81,7 +76,6 @@ describe('srcset', () => { }} aspect={300/200} alt=''/>) - cy.wait('@asset') // Get one of the sizes that should be been rendered cy.get('[srcset]').invoke('attr', 'srcset') @@ -102,7 +96,6 @@ describe('srcset', () => { aspect={300/200} sizes='100vw' alt=''/>) - cy.wait('@asset') cy.get('[srcset]').invoke('attr', 'srcset') .should('not.contain', ' 16w') @@ -110,9 +103,6 @@ describe('srcset', () => { it('it adds narrower widths with sizes prop', () => { - // This is the particular image we expect to load - cy.intercept('https://placehold.co/256x256').as('50vw') - cy.mount( { @@ -122,14 +112,10 @@ describe('srcset', () => { width='50%' sizes='50vw' alt=''/>) - cy.wait('@asset') - - // The image that should have been loaded - cy.get('[srcset]').invoke('attr', 'srcset') - .should('contain', 'https://placehold.co/256x256 256w') - // Ensure that we didn't load too big or small of an image - cy.wait('@50vw') + // Should be half width + cy.get('img').its('[0].currentSrc') + .should('eq', 'https://placehold.co/256x256') }) @@ -139,8 +125,6 @@ describe('sources', () => { it('supports rendering sources for mimetypes', () => { - cy.intercept('https://placehold.co/640x640.webp').as('webp') - cy.mount( { }} aspect={1} alt=''/>) - cy.wait('@asset') - cy.wait('@webp') + // Should be webp source + cy.get('img').its('[0].currentSrc') + .should('eq', 'https://placehold.co/640x640.webp') }) it('supports rendering sources for mimetypes and media queries', () => { - cy.intercept('https://placehold.co/640x320.webp').as('landscape') - cy.intercept('https://placehold.co/640x640.webp').as('portrait') - // Start at a landscape viewport cy.viewport(500, 400) @@ -179,14 +161,15 @@ describe('sources', () => { }} width='100%' alt=''/>) - cy.wait('@asset') - // Landscape should have loaded - cy.wait('@landscape') + // Should be landscape source + cy.get('img').its('[0].currentSrc') + .should('eq', 'https://placehold.co/640x320.webp') // Switch to portrait, which should load the other source cy.viewport(500, 600) - cy.wait('@portrait') + cy.get('img').its('[0].currentSrc') + .should('eq', 'https://placehold.co/640x640.webp') }) From 8f3728b1506a940143c0e9ae1a54c9ae7cb74eb9 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 16 Nov 2023 16:58:31 -0800 Subject: [PATCH 22/29] Add docs --- packages/react/README.md | 109 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/packages/react/README.md b/packages/react/README.md index a64616b..926dcbe 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1,3 +1,110 @@ # @react-visual/react [![react-visual](https://img.shields.io/endpoint?url=https://cloud.cypress.io/badge/simple/fn6c7w&style=flat&logo=cypress)](https://cloud.cypress.io/projects/fn6c7w/runs) -This component package isn't fully implemented yet, it's just olding some shared components but not ready to be implemented on it's own. + +Renders images and videos into a container. Features: + +- Supports a next.js style image loader for making srcsets +- Creates `` tags for different MIME types and media queries +- Easily render assets using aspect ratios +- Videos are lazyloaded (unless `priority` flag is set) + +## Install + +```sh +yarn add @react-visual/react +``` + +## Usage + +Play a video with a poster image. + +```jsx +import Visual from '@react-visual/react' + +export default function VideoExample() { + return ( + + ) +} +``` + +Generate multiple landscape and portrait sources in webp and avif using an image CDN to create a srcset. + +```jsx +import Visual from '@react-visual/react' + +export default function ResponsiveExample() { + return ( + { + const ext = type?.includes('webp') ? '.webp' : '' + const height = media?.includes('landscape') ? + width * 0.5 : width + return `https://placehold.co/${width}x${height}${ext}` + }} + aspect={300/150} + sizes='100vw' + alt='Example of responsive images' /> + ) +} +``` + +For more examples, read [the Cypress component tests](./cypress/component). + +## Props + +### Sources + +| Prop | Type | Description +| -- | -- | -- +| `image` | `string` | URL to an image asset. +| `video` | `string` | URL to a video asset asset. + +### Layout + +| Prop | Type | Description +| -- | -- | -- +| `expand` | `boolean` | Make the Visual fill it's container via CSS using absolute positioning. +| `aspect` | `number` | Force the Visual to a specific aspect ratio. +| `width` | `number`, `string` | A CSS dimension value or a px number. +| `height` | `number`, `string` | A CSS dimension value or a px number. +| `fit` | `string` | An [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) value that is applied to the assets. Defaults to `cover`. +| `position` | `string` | An [`object-position`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) value. + +### Loading + +| Prop | Type | Description +| -- | -- | -- +| `priority` | `boolean` | Disables [`` attribute. +| `sourceTypes` | `string[]` | Specify image MIME types that will be passed to the `imageLoader` and used to create additional `` tags. Use this to create `webp` or `avif` sources with a CDN like Contentful. +| `sourceMedia` | `string[]` | Specify media queries that will be passed to the `imageLoader` and used to create additional `` tags. +| `imageLoader` | `Function` | Uses syntax that is similar [to `next/image`'s `loader` prop](https://nextjs.org/docs/app/api-reference/components/image#loader). A srcset is built with a hardcoded list of widths. + +### Video + +| Prop | Type | Description +| -- | -- | -- +| `paused` | `boolean` | Disables autoplay of videos. This prop is reactive, unlike the `paused` property of the html `