-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of github.com:BKWLD/next-visual
- Loading branch information
Showing
18 changed files
with
684 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
|
||
}) | ||
|
||
}) |
Oops, something went wrong.