Skip to content

Commit

Permalink
Merge pull request #36 from BKWLD/improve-responsive-support
Browse files Browse the repository at this point in the history
Improve responsive support
  • Loading branch information
weotch committed Nov 27, 2023
2 parents a93c7d9 + 4cf383a commit f7ff122
Show file tree
Hide file tree
Showing 16 changed files with 470 additions and 112 deletions.
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,16 @@ import Visual from '@react-visual/react'
export default function ResponsiveExample() {
return (
<Visual
image="https://placehold.co/300x150"
sourceTypes={["image/webp", "image/jpeg"]}
sourceMedia={["(orientation:landscape)", "(orientation:portrait)"]}
imageLoader={({ type, media, width }) => {
const ext = type?.includes("webp") ? ".webp" : ".jpg";
const height = media?.includes("landscape") ? width * 0.5 : width;
return `https://placehold.co/${width}x${height}${ext}`;
image='https://placehold.co/200x200'
sourceTypes={['image/webp']}
sourceMedia={['(orientation:landscape)', '(orientation:portrait)']}
imageLoader={({ src, type, media, width }) => {
const height = media?.includes('landscape') ? width * 0.5 : width
const ext = type?.includes('webp') ? '.webp' : ''
return `https://placehold.co/${width}x${height}${ext}`
}}
aspect={300 / 150}
sizes="100vw"
alt="Example of responsive images"
/>
width='100%'
alt='Example of responsive images'/>
)
}
```
Expand Down
139 changes: 109 additions & 30 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function VideoExample() {
}
```

Generate multiple landscape and portrait sources in webp and avif using an image CDN to create a srcset.
Generate multiple landscape and portrait sources using an image CDN to create a srcset.

```jsx
import Visual from '@react-visual/react'
Expand All @@ -42,11 +42,11 @@ export default function ResponsiveExample() {
return (
<Visual
image='https://placehold.co/200x200'
sourceTypes={['image/webp', 'image/jpeg']}
sourceTypes={['image/webp']}
sourceMedia={['(orientation:landscape)', '(orientation:portrait)']}
imageLoader={({ src, type, media, width }) => {
const height = media?.includes('landscape') ? width * 0.5 : width
const ext = type?.includes('webp') ? '.webp' : '.jpg'
const ext = type?.includes('webp') ? '.webp' : ''
return `https://placehold.co/${width}x${height}${ext}`
}}
width='100%'
Expand All @@ -58,30 +58,109 @@ export default function ResponsiveExample() {
The above would produce:
```html
<picture>
<source
type='image/webp'
media='(orientation:landscape)'
srcset='https://placehold.co/640x320.webp 640w, https://placehold.co/750x375.webp 750w, https://placehold.co/828x414.webp 828w, https://placehold.co/1080x540.webp 1080w, https://placehold.co/1200x600.webp 1200w, https://placehold.co/1920x960.webp 1920w, https://placehold.co/2048x1024.webp 2048w, https://placehold.co/3840x1920.webp 3840w'>
<source
type='image/webp'
media='(orientation:portrait)'
srcset='https://placehold.co/640x640.webp 640w, https://placehold.co/750x750.webp 750w, https://placehold.co/828x828.webp 828w, https://placehold.co/1080x1080.webp 1080w, https://placehold.co/1200x1200.webp 1200w, https://placehold.co/1920x1920.webp 1920w, https://placehold.co/2048x2048.webp 2048w, https://placehold.co/3840x3840.webp 3840w'>
<source
type='image/jpeg'
media='(orientation:landscape)'
srcset='https://placehold.co/640x320.jpg 640w, https://placehold.co/750x375.jpg 750w, https://placehold.co/828x414.jpg 828w, https://placehold.co/1080x540.jpg 1080w, https://placehold.co/1200x600.jpg 1200w, https://placehold.co/1920x960.jpg 1920w, https://placehold.co/2048x1024.jpg 2048w, https://placehold.co/3840x1920.jpg 3840w'>
<source
type='image/jpeg'
media='(orientation:portrait)'
srcset='https://placehold.co/640x640.jpg 640w, https://placehold.co/750x750.jpg 750w, https://placehold.co/828x828.jpg 828w, https://placehold.co/1080x1080.jpg 1080w, https://placehold.co/1200x1200.jpg 1200w, https://placehold.co/1920x1920.jpg 1920w, https://placehold.co/2048x2048.jpg 2048w, https://placehold.co/3840x3840.jpg 3840w'>
<img
src='https://placehold.co/200x200'
loading='lazy'
alt='Example of responsive images'
srcset='https://placehold.co/640x640.jpg 640w, https://placehold.co/750x750.jpg 750w, https://placehold.co/828x828.jpg 828w, https://placehold.co/1080x1080.jpg 1080w, https://placehold.co/1200x1200.jpg 1200w, https://placehold.co/1920x1920.jpg 1920w, https://placehold.co/2048x2048.jpg 2048w, https://placehold.co/3840x3840.jpg 3840w'
style='object-fit: cover; width: 100%;'>
</picture>
<div style="position: relative; width: 100%; max-width: 100%;">
<picture>
<source
type='image/webp'
media='(orientation:landscape)'
srcset='https://placehold.co/640x320.webp 640w, https://placehold.co/750x375.webp 750w, https://placehold.co/828x414.webp 828w, https://placehold.co/1080x540.webp 1080w, https://placehold.co/1200x600.webp 1200w, https://placehold.co/1920x960.webp 1920w, https://placehold.co/2048x1024.webp 2048w, https://placehold.co/3840x1920.webp 3840w'>
<source
type='image/webp'
media='(orientation:portrait)'
srcset='https://placehold.co/640x640.webp 640w, https://placehold.co/750x750.webp 750w, https://placehold.co/828x828.webp 828w, https://placehold.co/1080x1080.webp 1080w, https://placehold.co/1200x1200.webp 1200w, https://placehold.co/1920x1920.webp 1920w, https://placehold.co/2048x2048.webp 2048w, https://placehold.co/3840x3840.webp 3840w'>
<source
type='image/webp'
media='(orientation:landscape)'
srcset='https://placehold.co/640x320 640w, https://placehold.co/750x375 750w, https://placehold.co/828x414 828w, https://placehold.co/1080x540 1080w, https://placehold.co/1200x600 1200w, https://placehold.co/1920x960 1920w, https://placehold.co/2048x1024 2048w, https://placehold.co/3840x1920 3840w'>
<source
media='(orientation:portrait)'
srcset='https://placehold.co/640x640 640w, https://placehold.co/750x750 750w, https://placehold.co/828x828 828w, https://placehold.co/1080x1080 1080w, https://placehold.co/1200x1200 1200w, https://placehold.co/1920x1920 1920w, https://placehold.co/2048x2048 2048w, https://placehold.co/3840x3840 3840w'>
<img
src='https://placehold.co/200x200'
loading='lazy'
alt='Example of responsive images'
style='object-fit: cover; width: 100%;'>
</picture>
</div>
```
Accept objects from a CMS to produce responsive assets at different aspect ratios.
```jsx
import Visual from '@react-visual/react'

export default function ResponsiveExample() {
return (
<Visual
image={{
landscape: {
url: 'https://placehold.co/500x250',
aspect: 2,
},
portrait: {
url: 'https://placehold.co/500x500',
aspect: 1,
}
}}
sourceMedia={['(orientation: landscape)', '(orientation: portrait)']}
imageLoader={({ src, type, media, width }) => {

// Choose the right source
const asset = media?.includes('landscape') ?
src.landscape : src.portrait

// Make the dimensions
const dimensions = `${width}x${width / asset.aspect}`

// Choose the right format
const ext = type?.includes('webp') ? '.webp' : '.jpg'

// Make the url
return `https://placehold.co/${dimensions}${ext}`

}}
aspect={({ image, media }) => {
return media?.includes('landscape') ?
image.landscape.aspect :
image.portrait.aspect
}}
alt='Example of responsive images'/>
)
}
```
This produces:
```html
<div
class="rv-orientation-landscape-2 rv-orientation-portrait-1"
style="position: relative; max-width: 100%;">
<picture>
<source
media="(orientation: landscape)"
srcset="https://placehold.co/640x320.jpg 640w, https://placehold.co/750x375.jpg 750w, https://placehold.co/828x414.jpg 828w, https://placehold.co/1080x540.jpg 1080w, https://placehold.co/1200x600.jpg 1200w, https://placehold.co/1920x960.jpg 1920w, https://placehold.co/2048x1024.jpg 2048w, https://placehold.co/3840x1920.jpg 3840w">
<source
media="(orientation: portrait)"
srcset="https://placehold.co/640x640.jpg 640w, https://placehold.co/750x750.jpg 750w, https://placehold.co/828x828.jpg 828w, https://placehold.co/1080x1080.jpg 1080w, https://placehold.co/1200x1200.jpg 1200w, https://placehold.co/1920x1920.jpg 1920w, https://placehold.co/2048x2048.jpg 2048w, https://placehold.co/3840x3840.jpg 3840w">
<img
src="https://placehold.co/1920x1920.jpg"
loading="lazy"
alt="Example of responsive images"
style="object-fit: cover; position: absolute; inset: 0px;">
</picture>
<style>
@media (orientation: landscape) {
.rv-orientation-landscape-2 {
aspect-ratio: 2;
}
}
@media (orientation: portrait) {
.rv-orientation-portrait-1 {
aspect-ratio: 1;
}
}
</style>
</div>
```
For more examples, read [the Cypress component tests](./cypress/component).
Expand All @@ -92,15 +171,15 @@ For more examples, read [the Cypress component tests](./cypress/component).
| Prop | Type | Description
| -- | -- | --
| `image` | `string` | URL to an image asset.
| `video` | `string` | URL to a video asset asset.
| `image` | `string`, `object` | URL to an image asset.
| `video` | `string`, `object` | 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.
| `aspect` | `number`, `function` | 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`.
Expand Down
83 changes: 80 additions & 3 deletions packages/react/cypress/component/ReactVisual.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,16 @@ describe('sources', () => {
return `https://placehold.co/${width}x${width}${ext}`
}}
aspect={1}
width='50%'
sizes='50vw'
alt=''/>)

// Should be webp source
// Should be webp source at reduced width
cy.get('img').its('[0].currentSrc')
.should('eq', 'https://placehold.co/640x640.webp')
.should('eq', 'https://placehold.co/256x256.webp')

// It should also have a fallback source
cy.get('source:not([type])').should('have.length', 1)

})

Expand All @@ -151,7 +156,7 @@ describe('sources', () => {

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

Expand All @@ -174,6 +179,78 @@ describe('sources', () => {
cy.get('img').its('[0].currentSrc')
.should('eq', 'https://placehold.co/640x640.webp')

// There should be fallback sources (non-web) for each orientation
cy.get('source').should('have.length', 4)
cy.get('source:not([type])').should('have.length', 2)
})

it('supports rendering object based sources', () => {

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

cy.mount(<ReactVisual
image={{
landscape: {
url: 'https://placehold.co/500x255?text=landscape+image',
aspect: 2,
},
portrait: {
url: 'https://placehold.co/500x505?text=portrait+image',
aspect: 1,
}
}}
sourceTypes={['image/webp']}
sourceMedia={['(orientation: landscape)', '(orientation: portrait)']}
imageLoader={({ src, type, media, width }) => {

// Choose the right source
const asset = media?.includes('landscape') ?
src.landscape : src.portrait

// Make the dimensions
const dimensions = `${width}x${width / asset.aspect}`

// Choose the right format
const ext = type?.includes('webp') ? '.webp' : '.jpg'

// Get text message from src url
const text = (new URL(asset.url)).searchParams.get('text')
+ `\\n${dimensions}${ext}`

// Make the url
return `https://placehold.co/${dimensions}${ext}?text=`+
encodeURIComponent(text)
}}
aspect={({ image, media }) => {
return media?.includes('landscape') ?
image.landscape.aspect :
image.portrait.aspect
}}
data-cy='react-visual'
alt=''/>)

// Generates a default from the first asset found
cy.get('img').invoke('attr', 'src')
.should('contain', 'https://placehold.co/1920x1920')

// Expect a landscape image
cy.get('img').its('[0].currentSrc')
.should('contain', 'https://placehold.co/640x320')
.should('contain', 'landscape')

// Check that the aspect is informing the size, not the image size
cy.get('[data-cy=react-visual]').hasDimensions(500, 250)

// Switch to portrait, which should load the other source
cy.viewport(500, 600)
cy.get('img').its('[0].currentSrc')
.should('contain', 'https://placehold.co/640x640')
.should('contain', 'portrait')

// Check aspect again
cy.get('[data-cy=react-visual]').hasDimensions(500, 500)

})

})
29 changes: 28 additions & 1 deletion packages/react/cypress/component/VisualWrapper.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const sharedProps = {
style: { background: 'black', color: 'white' },
className: 'wrapper',
}
const style = { background: 'black' }

// Viewport sizes
const VW = Cypress.config('viewportWidth'),
Expand Down Expand Up @@ -46,6 +45,34 @@ it('supports aspect', () => {
cy.get('.wrapper').hasDimensions(VW, VH / 2)
})

it('supports respponsive aspect function', () => {
cy.mount(<VisualWrapper
{...sharedProps }
image={{
landscape: {
aspect: 2,
},
portrait: {
aspect: 1,
}
}}
sourceMedia={[
'(orientation: landscape)',
'(orientation: portrait)'
]}
aspect={({ image, media }) => {
return media?.includes('landscape') ?
image.landscape.aspect :
image.portrait.aspect
}}
/>)
cy.viewport(500, 400)
cy.get('.wrapper').hasDimensions(500, 250)
cy.viewport(400, 500)
cy.get('.wrapper').hasDimensions(400, 400)
})


it('supports children', () => {
cy.mount(<VisualWrapper {...sharedProps }>
<h1>Hey</h1>
Expand Down
1 change: 1 addition & 0 deletions packages/react/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Cypress.Commands.add('hasDimensions',
Cypress.Commands.add('imgLoaded',
{ prevSubject: true },
(subject) => {
cy.wait(100) // Wait a tick to solve for inexplicable flake
cy.wrap(subject)
.should('be.visible')
.and('have.prop', 'naturalWidth')
Expand Down
Loading

0 comments on commit f7ff122

Please sign in to comment.