Skip to content

Commit

Permalink
Merge pull request #40 from BKWLD/contentful-visual
Browse files Browse the repository at this point in the history
Implement Contentful Visual
  • Loading branch information
weotch authored Nov 30, 2023
2 parents 0fe85b4 + 2794d13 commit e9f1417
Show file tree
Hide file tree
Showing 22 changed files with 657 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ jobs:
strategy:
matrix:
dir:
- packages/next
- packages/react
- packages/contentful
- packages/next
- packages/sanity-next

steps:
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
A monorepo hosting components for rendering image and video in a single container for easy rendering of visual elements.

- [@react-visual/react](./packages/react) - Vanilla implementation.
- [@react-visual/contentful](./packages/contentful) - Adapter for Contentful assets.
- [@react-visual/next](./packages/next) - Uses the `next/image` component for rendering images.
- [@react-visual/sanity-next](./packages/sanity-next) - Takes Sanity asset and passes them to `@react-visual/next` for rendering.

Expand Down Expand Up @@ -34,6 +35,22 @@ export default function ResponsiveExample() {
[View CodeSandbox demo](https://codesandbox.io/p/sandbox/react-visual-react-demo-w4sh62)
### @react-visual/contentful
Using with a Visual entryType containing image and video fields:
```jsx
import Visual from '@react-visual/contentful'

export default function Example() {
return (
<Visual
src={ entry.background }
sizes='100vw' />
)
}
```
### @react-visual/next
Using framework adapter for Next.js:
Expand Down
128 changes: 128 additions & 0 deletions packages/contentful/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# @react-visual/contentful [![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)

Renders Contentful images and videos into a container. Features:

- Automatically defines a loader functions for generating srcsets
- Supports responsive image and video assets

## Install

```sh
yarn add @react-visual/contentful
```

## Usage

### Asset fields

```jsx
import Visual from '@react-visual/contentful'

export default function Example() {
return (
<Visual
image={ entry.image }
video={ entry.video }
sizes='100vw'/>
)
}
```

Where `image` and `video` are asset fields defined by these GQL fragments:

```gql
fragment image on Asset {
title
description
fileName
width
height
url
}

fragment video on Asset {
title
description
fileName
url
}
```

### Visual entryType reference

This is the expected pattern for rendering responsive images and videos.

```jsx
import Visual from '@react-visual/contentful'

export default function Example() {
return (
<Visual
src={ entry.background }
sizes='100vw'/>
)
}
```

Where `background` is defined by this GQL fragment (this consumes the previous fragments):

```gql
fragment visual on Visual {
image { ...image }
portraitImage { ...image }
video { ...video }
portraitVideo { ...video }
alt
}
```

For more examples, read [the Cypress component tests](./cypress/component).

## Props

### Sources

| Prop | Type | Description
| -- | -- | --
| `image` | `object` | A Contentful image Asset.
| `video` | `object` | A Contentful video Asset.
| `src` | `object` | An object with keys of responsive keys. See examples above.

### 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. If empty, this will be set using width and height fields from Contentful queries.
| `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` | Sets [`next/image`'s `priority`](https://nextjs.org/docs/pages/api-reference/components/image#priority) and videos to not lazy load.
| `sizes` | `string` | Sets [`next/image`'s `sizes`](https://nextjs.org/docs/pages/api-reference/components/image#sizes) prop.
| `imageLoader` | `Function` | This is passed through [to `next/image`'s `loader` prop](https://nextjs.org/docs/app/api-reference/components/image#loader).

### 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.
13 changes: 13 additions & 0 deletions packages/contentful/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from "cypress";

export default defineConfig({
viewportWidth: 500,
viewportHeight: 500,
component: {
specPattern: "cypress/component/**/*.cy.{js,jsx,ts,tsx}",
devServer: {
framework: "next",
bundler: "webpack",
},
},
});
116 changes: 116 additions & 0 deletions packages/contentful/cypress/component/ContentfulVisual.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import ContentfulVisual from '../../src'
import {
imageAsset,
portraitImageAsset,
videoAsset,
portraitVideoAsset
} from '../fixtures/assets'
import { visualEntry } from '../fixtures/entries'

// Dimensions
const VW = Cypress.config('viewportWidth'),
VH = Cypress.config('viewportHeight'),
landscapeAspect = imageAsset.width / imageAsset.height

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

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

})

describe('contentful asset props', () => {

it('renders image', () => {
cy.mount(<ContentfulVisual image={ imageAsset } />)
cy.get('img')
.hasDimensions(VW, VW / landscapeAspect)
.invoke('attr', 'alt').should('eq', imageAsset.title)
cy.get('img').its('[0].currentSrc').should('contain', 'w=640') // srcset
})

it('can override inferred props', () => {
cy.mount(<ContentfulVisual
image={ imageAsset }
aspect={ 1 }
alt='Override' />)
cy.get('img')
.hasDimensions(VW, VW)
.invoke('attr', 'alt').should('eq', 'Override')
})

it('renders video', () => {
cy.mount(<ContentfulVisual video={ videoAsset } aspect={ 16/9 } />)
cy.get('video')
.hasDimensions(VW, VW / (16/9) )
.invoke('attr', 'aria-label').should('eq', videoAsset.description)
})

})

describe('contentful visual entry props', () => {

it('renders responsive images', () => {
cy.mount(<ContentfulVisual src={{
...visualEntry,
video: null,
portraitVideo: null,
}} />)

// Portrait asset
cy.get('img').hasDimensions(VW, VW)
cy.get('img').its('[0].currentSrc')
.should('contain', 'w=640')
.should('contain', portraitImageAsset.url)

// Landscape asset
cy.viewport(500, 400)
cy.get('img').hasDimensions(VW, VW / landscapeAspect)
cy.get('img').its('[0].currentSrc')
.should('contain', 'w=640')
.should('contain', imageAsset.url)
})

it('renders responsive videos', () => {
cy.mount(<ContentfulVisual expand src={{
...visualEntry,
image: null,
portraitImage: null,
}} />)

// Portrait asset
cy.get('video').its('[0].currentSrc')
.should('contain', portraitVideoAsset.url)

// Landscape asset
cy.viewport(500, 400)
cy.get('video').its('[0].currentSrc')
.should('contain', videoAsset.url)
})

it('renders full visual entry', () => {
cy.mount(<ContentfulVisual src={visualEntry} />)

// Portrait asset
cy.get('img').hasDimensions(VW, VW)
cy.get('img').its('[0].currentSrc')
.should('contain', portraitImageAsset.url)
cy.get('video').its('[0].currentSrc')
.should('contain', portraitVideoAsset.url)

// Landscape asset
cy.viewport(500, 400)
cy.get('img').hasDimensions(VW, VW / landscapeAspect)
cy.get('img').its('[0].currentSrc')
.should('contain', imageAsset.url)
cy.get('video').its('[0].currentSrc')
.should('contain', videoAsset.url)
})

})
31 changes: 31 additions & 0 deletions packages/contentful/cypress/fixtures/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const imageAsset = {
title: "Landscape gradient",
description: "",
fileName: "landscape.png",
width: 1280,
height: 720,
url: "https://images.ctfassets.net/x9fe3hhqauxm/6WN8Zz47zmXelBLDenfteO/7e87ed7484738fd97fd86ac67f6feed5/tumblr_n8xc9t24W31tf8vylo1_1280.png"
}

export const portraitImageAsset = {
title: "Square gradient",
description: null,
fileName: "square.png",
width: 1280,
height: 1280,
url: "https://images.ctfassets.net/x9fe3hhqauxm/1lsKAmirOYNDVu0bfCfQ2H/32da927183924f22c535a74a10512817/tumblr_n8z97bAEMU1tf8vylo1_1280.png"
}

export const videoAsset = {
title: "Background Loop",
description: "Background loop description",
fileName: "Backround_Loop.mp4",
url: "https://videos.ctfassets.net/x9fe3hhqauxm/3TMXSh7C5nR1lUOeUygMou/15eccf4eaddcf1289c5b2b9ca83c90c4/PANDORA_ANIMATION_5.mp4"
}

export const portraitVideoAsset = {
title: "Portrait Loop",
description: "",
fileName: "Portait_Loop.mp4",
url: "https://videos.ctfassets.net/x9fe3hhqauxm/2mveMy2NaxbwfIp6ABl8xc/641f890eb10a329766d52d4805d404b4/PANDORA_ANIMATION_4.mp4"
}
14 changes: 14 additions & 0 deletions packages/contentful/cypress/fixtures/entries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
imageAsset,
portraitImageAsset,
videoAsset,
portraitVideoAsset
} from './assets'

export const visualEntry = {
image: imageAsset,
portraitImage: portraitImageAsset,
video: videoAsset,
portraitVideo: portraitVideoAsset,
alt: 'Description',
}
38 changes: 38 additions & 0 deletions packages/contentful/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/// <reference types="cypress" />

// Asset that el has dimensions
Cypress.Commands.add('hasDimensions',
{ prevSubject: true },
(subject, width, height) => {
cy.wrap(subject).invoke('width').should('equal', width)
cy.wrap(subject).invoke('height').should('equal', height)
cy.wrap(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)
cy.wrap(subject)
})

// 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<void>

isPlaying(): Chainable<void>
}
}
}


Loading

0 comments on commit e9f1417

Please sign in to comment.