diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml
index 65e538e..cba8281 100644
--- a/.github/workflows/cypress.yml
+++ b/.github/workflows/cypress.yml
@@ -10,6 +10,7 @@ jobs:
matrix:
dir:
- packages/next
+ - packages/react
- packages/sanity-next
steps:
diff --git a/README.md b/README.md
index 33e4421..254459d 100644
--- a/README.md
+++ b/README.md
@@ -45,5 +45,7 @@ export default function Example({ background }) {
This project uses Lerna to release versions, using [the default versioning strategy](https://lerna.js.org/docs/features/version-and-publish#versioning-strategies).
+- `yarn lerna run test` - Run all tests
- `yarn lerna version` - Tag a new version
-- `yarn lerna publish from-package` - Publish versions to NPM
+- `yarn lerna publish [major|minor|path]` - Tag and publish a version
+- `yarn lerna publish from-package` - Publish the current version
diff --git a/packages/next/cypress/support/commands.ts b/packages/next/cypress/support/commands.ts
index 6307497..012519e 100644
--- a/packages/next/cypress/support/commands.ts
+++ b/packages/next/cypress/support/commands.ts
@@ -15,7 +15,7 @@ Cypress.Commands.add('hasDimensions',
Cypress.Commands.add('isPlaying',
{ prevSubject: true },
(subject) => {
- cy.wrap(subject).should('have.prop', 'paused', true)
+ cy.wrap(subject).should('have.prop', 'paused', false)
})
// Add Typescript support for custom commaands
diff --git a/packages/next/src/NextVisual.tsx b/packages/next/src/NextVisual.tsx
index 8432227..198f1bf 100644
--- a/packages/next/src/NextVisual.tsx
+++ b/packages/next/src/NextVisual.tsx
@@ -29,6 +29,7 @@ export default function NextVisual(
priority,
sizes,
imageLoader,
+ playing,
alt,
className = '',
style = {},
@@ -67,7 +68,8 @@ export default function NextVisual(
fit,
position,
priority,
- noPoster: !!image // Use `image` as poster frame
+ noPoster: !!image, // Use `image` as poster frame
+ playing,
}} /> }
diff --git a/packages/next/src/types/nextVisualTypes.ts b/packages/next/src/types/nextVisualTypes.ts
index 8a23dfa..bd3bf06 100644
--- a/packages/next/src/types/nextVisualTypes.ts
+++ b/packages/next/src/types/nextVisualTypes.ts
@@ -17,6 +17,8 @@ export type NextVisualProps = {
sizes?: string
imageLoader?: Function
+ playing?: boolean
+
alt: string
className?: string
diff --git a/packages/react/cypress.config.ts b/packages/react/cypress.config.ts
new file mode 100644
index 0000000..27593d4
--- /dev/null
+++ b/packages/react/cypress.config.ts
@@ -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",
+ },
+ },
+});
diff --git a/packages/react/cypress/component/LazyVideo.cy.tsx b/packages/react/cypress/component/LazyVideo.cy.tsx
new file mode 100644
index 0000000..5313caa
--- /dev/null
+++ b/packages/react/cypress/component/LazyVideo.cy.tsx
@@ -0,0 +1,42 @@
+import { useState } from 'react'
+import { LazyVideo } from '../../src'
+
+// Make an instance of LazyVideo that can be controlled
+const Player = function({ autoplay }: any) {
+
+ const [playing, setPlaying] = useState(autoplay)
+
+ return (<>
+
+
+
+
+
+ >)
+}
+
+
+describe('playback', () => {
+
+ it('can be paused and then restarted', () => {
+ cy.mount()
+ cy.get('video').isPlaying()
+ cy.get('button').contains('Pause').click()
+ cy.get('video').isPaused()
+ cy.get('button').contains('Play').click()
+ cy.get('video').isPlaying()
+ })
+
+ it('can be started and then paused', () => {
+ cy.mount()
+ cy.get('video').isPaused()
+ cy.get('button').contains('Play').click()
+ cy.get('video').isPlaying()
+ cy.get('button').contains('Pause').click()
+ cy.get('video').isPaused()
+ })
+
+})
diff --git a/packages/react/cypress/support/commands.ts b/packages/react/cypress/support/commands.ts
new file mode 100644
index 0000000..062cc2c
--- /dev/null
+++ b/packages/react/cypress/support/commands.ts
@@ -0,0 +1,37 @@
+///
+
+// 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)
+})
+
+// Check that a video is playing
+// https://glebbahmutov.com/blog/test-video-play/
+Cypress.Commands.add('isPaused',
+ { prevSubject: true },
+ (subject) => {
+ cy.wrap(subject).should('have.prop', 'paused', true)
+})
+
+// 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
+
+ isPlaying(): Chainable
+ isPaused(): Chainable
+ }
+ }
+}
+
+
diff --git a/packages/react/cypress/support/component-index.html b/packages/react/cypress/support/component-index.html
new file mode 100644
index 0000000..e9a4010
--- /dev/null
+++ b/packages/react/cypress/support/component-index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Components App
+
+
+
+
+
+
+
+
diff --git a/packages/react/cypress/support/component.ts b/packages/react/cypress/support/component.ts
new file mode 100644
index 0000000..41b935d
--- /dev/null
+++ b/packages/react/cypress/support/component.ts
@@ -0,0 +1,21 @@
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+import { mount } from 'cypress/react18'
+
+// Augment the Cypress namespace to include type definitions for
+// your custom command.
+// Alternatively, can be defined in cypress/support/component.d.ts
+// with a at the top of your spec.
+declare global {
+ namespace Cypress {
+ interface Chainable {
+ mount: typeof mount
+ }
+ }
+}
+
+Cypress.Commands.add('mount', mount)
diff --git a/packages/react/cypress/tsconfig.json b/packages/react/cypress/tsconfig.json
new file mode 100644
index 0000000..537c251
--- /dev/null
+++ b/packages/react/cypress/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "jsx": "preserve",
+ "lib": [
+ "es5",
+ "dom"
+ ],
+ "types": [
+ "cypress",
+ "node"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx"
+ ]
+}
diff --git a/packages/react/package.json b/packages/react/package.json
index cf60e04..6c426b7 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -10,7 +10,8 @@
"dist/**/*"
],
"scripts": {
- "build": "rm -rf dist/* && tsc"
+ "build": "rm -rf dist/* && tsc",
+ "test": "cypress run --component"
},
"dependencies": {
"react-intersection-observer": "^9"
diff --git a/packages/react/src/LazyVideo.tsx b/packages/react/src/LazyVideo.tsx
index 12e544f..8cbcbaf 100644
--- a/packages/react/src/LazyVideo.tsx
+++ b/packages/react/src/LazyVideo.tsx
@@ -2,20 +2,54 @@
"use client";
import { useInView } from 'react-intersection-observer'
-import type { ReactElement } from 'react'
+import { useEffect, type ReactElement, useRef, useCallback } from 'react'
+import type { LazyVideoProps } from './types/lazyVideoTypes';
import { fillStyles, transparentGif } from './lib/styles'
// An video rendered within a Visual that supports lazy loading
export default function LazyVideo({
- src, alt, fit, position, priority, noPoster
-}: any): ReactElement {
+ src, alt, fit, position, priority, noPoster, playing = true
+}: LazyVideoProps): ReactElement {
+
+ // Make a ref to the video so it can be controlled
+ const videoRef = useRef()
// Watch for in viewport to load video unless using priority
- const { ref, inView } = useInView({
+ const { ref: inViewRef, inView } = useInView({
skip: priority
})
+ // Support multiple refs on the video. This is from the
+ // react-intersection-observer docs
+ const setRefs = useCallback((node: HTMLVideoElement) => {
+ videoRef.current = node
+ inViewRef(node)
+ }, [inViewRef])
+
+ // Store the promise that is returned from play to prevent errors when
+ // pause() is called while video is benginning to play.
+ const playPromise = useRef>()
+
+ // Play the video, waiting until it's safe to play it. And capture any
+ // errors while trying to play.
+ const play = async () => {
+ if (playPromise.current) await playPromise.current
+ try { playPromise.current = videoRef.current?.play()}
+ catch (e) { console.error(e) }
+ }
+
+ // Pause the video, waiting until it's safe to play it
+ const pause = async () => {
+ if (playPromise.current) await playPromise.current
+ videoRef.current?.pause()
+ }
+
+ // Respond to playing prop and call methods that control the video playback
+ useEffect(() => {
+ playing ? play() : pause()
+ }, [ playing ])
+
// Simplify logic for whether to load sources
const shouldLoad = priority || inView
@@ -24,17 +58,19 @@ export default function LazyVideo({
// Props that allow us to autoplay videos like a gif
playsInline
- autoPlay
muted
loop
+ // Whether to autoplay
+ autoPlay={ playing }
+
// Load a transparent gif as a poster if an `image` was specified so
// the image is used as poster rather than the first frame of video. This
// lets us all use responsive poster images (via `next/image`).
poster={ noPoster ? transparentGif : undefined }
// Straightforward props
- ref={ref}
+ ref={setRefs}
preload={ shouldLoad ? 'auto' : 'none' }
aria-label={ alt }
style={{
diff --git a/packages/react/src/types/lazyVideoTypes.ts b/packages/react/src/types/lazyVideoTypes.ts
new file mode 100644
index 0000000..aff6d15
--- /dev/null
+++ b/packages/react/src/types/lazyVideoTypes.ts
@@ -0,0 +1,21 @@
+import type { CSSProperties } from 'react'
+
+export type LazyVideoProps = {
+
+ // Source props
+ src: HTMLVideoElement['src']
+ alt: string
+
+ // Don't lazy load
+ priority?: boolean
+
+ // Use a transparent gif poster image
+ noPoster?: boolean
+
+ // Controls autoplaying and current playing state
+ playing?: boolean
+
+ // Display props
+ fit?: CSSProperties['objectFit']
+ position?: CSSProperties['objectPosition']
+}
diff --git a/packages/sanity-next/cypress/support/commands.ts b/packages/sanity-next/cypress/support/commands.ts
index 6307497..012519e 100644
--- a/packages/sanity-next/cypress/support/commands.ts
+++ b/packages/sanity-next/cypress/support/commands.ts
@@ -15,7 +15,7 @@ Cypress.Commands.add('hasDimensions',
Cypress.Commands.add('isPlaying',
{ prevSubject: true },
(subject) => {
- cy.wrap(subject).should('have.prop', 'paused', true)
+ cy.wrap(subject).should('have.prop', 'paused', false)
})
// Add Typescript support for custom commaands