Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FInish @react-visual/react #31

Merged
merged 29 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ea44a9d
Move main Visual type declaration into react-visual
weotch Oct 25, 2023
7b8b15c
Stub out ReactVisual initial tests
weotch Oct 25, 2023
4053e73
Use image and video rendering from next-visual
weotch Oct 25, 2023
27cc4cd
Fix type reference
weotch Oct 25, 2023
2300385
Implement picture rendering in child element
weotch Oct 26, 2023
865dc96
Initial srcset rendering implementation
weotch Oct 26, 2023
fd1cff0
Support width/height as numeric strings
weotch Nov 16, 2023
3719ef3
Generate srcset using Next.js conventions
weotch Nov 16, 2023
8b226d4
Expand to wrapper
weotch Nov 16, 2023
a0353eb
Fix srcset syntax
weotch Nov 16, 2023
9736316
Rename PictureImageProps
weotch Nov 16, 2023
041568e
Test of that correct image is loaded
weotch Nov 16, 2023
95d4ebb
Rename ReactVisualProps
weotch Nov 16, 2023
3346deb
Support rendering source tags from types and media
weotch Nov 16, 2023
fa07da4
Add tests of VisualWrapper
weotch Nov 16, 2023
c4231a2
Wait or assets to load
weotch Nov 16, 2023
33cd243
Add tests of natural image size
weotch Nov 16, 2023
a406796
Test that the wrapper dimensions override natural image size
weotch Nov 16, 2023
a005fc1
Move commands to a shared location
weotch Nov 16, 2023
eed7b90
Revert "Move commands to a shared location"
weotch Nov 17, 2023
c941437
Drop network intercepting strategy
weotch Nov 17, 2023
8f3728b
Add docs
weotch Nov 17, 2023
80718a7
Wait for videos to load
weotch Nov 17, 2023
ee82388
Improve hasDimensions command
weotch Nov 17, 2023
61cc74f
Check if element is visible before aserting dimensions
weotch Nov 17, 2023
b54f393
Test the complete property
weotch Nov 17, 2023
64e7a0e
Test if GitHub actions has problems with SVG
weotch Nov 17, 2023
7bdd906
Test image load using Cypress example
weotch Nov 17, 2023
5de91b4
Move test into command
weotch Nov 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions packages/next/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
Cypress.Commands.add('hasDimensions',
{ prevSubject: true },
(subject, width, height) => {

expect(subject.width()).to.equal(width)
expect(subject.height()).to.equal(height)
return subject
cy.wrap(subject).invoke('width').should('equal', width)
cy.wrap(subject).invoke('height').should('equal', height)
})

// Check that a video is playing
Expand All @@ -28,7 +26,7 @@ declare global {
hasDimensions(
width: number,
height: number
): Chainable<JQueryWithSelector>
): Chainable<void>

isPlaying(): Chainable<void>
}
Expand Down
7 changes: 2 additions & 5 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import NextVisual from './NextVisual'

export default NextVisual
export {
NextVisualProps,
ObjectFit,
ObjectFitOption,
} from './types/nextVisualTypes'
export { NextVisualProps } from './types/nextVisualTypes'
33 changes: 2 additions & 31 deletions packages/next/src/types/nextVisualTypes.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,5 @@
import type { CSSProperties } from 'react'
import type { ReactVisualProps } from '@react-visual/react'

export type NextVisualProps = {

image?: string
video?: string
export type NextVisualProps = ReactVisualProps & {
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',
}
109 changes: 108 additions & 1 deletion packages/react/README.md
Original file line number Diff line number Diff line change
@@ -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 `<source>` 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 (
<Visual
image='https://placehold.co/300x150'
video='https://placehold.co/300x150.mp4'
aspect={300/150}
sizes='100vw'
alt='Example using placeholder images' />
)
}
```

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 (
<Visual
image='https://placehold.co/300x150'
sourceTypes={['image/avif', 'image/webp']}
sourceMedia={['(orientation:landscape)', '(orientation:portrait)']}
imageLoader={({ type, media, width }) => {
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 [`<img loading="lazy>"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading) and prevents videos from lazy loading based on IntersectionObserver.
| `sizes` | `string` | Sets the `<img sizes>` attribute.
| `sourceTypes` | `string[]` | Specify image MIME types that will be passed to the `imageLoader` and used to create additional `<source>` 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 `<source>` 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 `<video>` tag. You can set it to `true` to pause a playing video or set it to `false` to play a paused video.


### Accessibility

| Prop | Type | Description
| -- | -- | --
| `alt` | `string` | Sets the alt attribute or aria-label value, depending on asset type.

### Theming

| Prop | Type | Description
| -- | -- | --
| `className` | `string` | Add a custom CSS class.
| `style` | `CSSProperties` | Add additional styles.
179 changes: 179 additions & 0 deletions packages/react/cypress/component/ReactVisual.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import ReactVisual from '../../src'

beforeEach(() => {
cy.clearCache()
})

describe('no asset', () => {

it('renders nothing', () => {
cy.mount(<ReactVisual
width={300}
height={200}
alt=''
data-cy='react-visual' />)
cy.get('[data-cy=react-visual]').should('not.exist')
})

})

describe('fixed size', () => {

it('renders image', () => {
cy.mount(<ReactVisual
image='https://placehold.co/600x400'
width={300}
height={200}
alt=''/>)
cy.get('img').hasDimensions(300, 200)
})

it('renders video', () => {
cy.mount(<ReactVisual
video='https://placehold.co/600x400.mp4'
width={300}
height={200}
alt=''/>)
cy.get('video').hasDimensions(300, 200)
cy.get('video').isPlaying()
cy.wait(100) // Wait for video play to finish before moving on
})

it('renders image & video', () => {
cy.mount(<ReactVisual
image='https://placehold.co/600x400/black/white'
video='https://placehold.co/600x400.mp4'
width={300}
height={200}
alt=''
data-cy='next-visual' />)
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()
cy.wait(100) // Wait for video play to finish before moving on
})
})

describe('natural size', () => {

it('renders image', () => {
cy.mount(<ReactVisual
image='https://placehold.co/200x200.png'
alt=''/>)
cy.get('img').imgLoaded()
cy.get('img').hasDimensions(200, 200)
})

})

describe('srcset', () => {

it('renders srset with no sizes prop', () => {

cy.mount(<ReactVisual
image='https://placehold.co/300x200'
imageLoader={({ src, width }) => {
const height = Math.round(width * 200 / 300)
return `https://placehold.co/${width}x${height}`
}}
aspect={300/200}
alt=''/>)

// Get one of the sizes that should be been rendered
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(<ReactVisual
image='https://placehold.co/300x200'
imageLoader={({ src, width }) => {
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', () => {

cy.mount(<ReactVisual
image='https://placehold.co/200x200'
imageLoader={({ src, width }) => {
return `https://placehold.co/${width}x${width}`
}}
aspect={1}
width='50%'
sizes='50vw'
alt=''/>)

// Should be half width
cy.get('img').its('[0].currentSrc')
.should('eq', 'https://placehold.co/256x256')

})

})

describe('sources', () => {

it('supports rendering sources for mimetypes', () => {

cy.mount(<ReactVisual
image='https://placehold.co/200x200'
sourceTypes={['image/webp']}
imageLoader={({ src, type, width }) => {
const ext = type?.includes('webp') ? '.webp' : ''
return `https://placehold.co/${width}x${width}${ext}`
}}
aspect={1}
alt=''/>)

// 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', () => {

// Start at a landscape viewport
cy.viewport(500, 400)

cy.mount(<ReactVisual
image='https://placehold.co/200x200'
sourceTypes={['image/webp']}
sourceMedia={['(orientation:landscape)', '(orientation:portrait)']}
imageLoader={({ src, type, media, width }) => {

// 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=''/>)

// 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.get('img').its('[0].currentSrc')
.should('eq', 'https://placehold.co/640x640.webp')

})

})
Loading