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

Implement Contentful Visual #40

Merged
merged 9 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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