Skip to content

Commit

Permalink
Merge branch 'main' of github.com:BKWLD/next-visual
Browse files Browse the repository at this point in the history
  • Loading branch information
weotch committed Nov 17, 2023
2 parents abd875c + ba0f360 commit b757e08
Show file tree
Hide file tree
Showing 18 changed files with 684 additions and 52 deletions.
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

0 comments on commit b757e08

Please sign in to comment.