Skip to content
Open
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
7 changes: 5 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
"extends": [
"plugin:@typescript-eslint/recommended",
"react-app",
"plugin:react-hooks/recommended",
"prettier",
"plugin:cypress/recommended"
],
"plugins": ["@typescript-eslint/eslint-plugin", "cypress"],
"plugins": ["@typescript-eslint/eslint-plugin", "cypress", "react-hooks"],
"rules": {
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/ban-ts-ignore": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/ban-types": 0
"@typescript-eslint/ban-types": 0,
"react-hooks/exhaustive-deps": "error",
"react-hooks/rule-suppression": "error"
}
}
9 changes: 9 additions & 0 deletions .github/workflows/quality-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
restore-keys: |
pnpm-deps-${{ hashFiles('pnpm-lock.yaml') }}
- run: pnpm install --frozen-lockfile
- name: Run type checking
run: pnpm type-check

- name: Create Build
run: pnpm build
Expand All @@ -47,6 +49,13 @@ jobs:
- name: Run unit tests
run: pnpm unit

# docs/package.json links ../dist so this validates the build from this commit.
- name: Install documentation dependencies against local build
run: pnpm --dir docs install --ignore-workspace --frozen-lockfile --ignore-scripts

- name: Build documentation
run: pnpm --dir docs build:site

- name: Cypress run
uses: cypress-io/github-action@v7
with:
Expand Down
99 changes: 52 additions & 47 deletions README.md

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions docs/docs/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Called when rotation changes through gestures or controlled UI.

## `onCropComplete`

Called when the user stops moving or zooming the media.
Called once after media initialization, when a mouse, touch, or keyboard interaction ends, and after wheel or resize activity has been quiet for 250 ms.

```tsx
function onCropComplete(croppedArea, croppedAreaPixels) {
Expand All @@ -53,9 +53,11 @@ Both arguments have this shape:

`croppedArea` is percentages. `croppedAreaPixels` is pixels.

Unrelated renders and values that calculate the same crop do not emit the callback again. If the cropper corrects a controlled crop or zoom value, crop callbacks run after the corrected value commits.

## `onCropAreaChange`

Same arguments as `onCropComplete`, but called during interaction instead of waiting for the interaction to end.
Same arguments as `onCropComplete`, but called once per committed crop change during interactions and relevant controlled updates instead of waiting for completion.

## `onMediaLoaded`

Expand All @@ -69,6 +71,10 @@ Called when the media loads.
/>
```

## Size changes

`onMediaSizeChange` receives `{ width, height, naturalWidth, naturalHeight }` when the measured media size changes. `onCropSizeChange` receives `{ width, height }` when the crop area size changes. Equal measurements and unrelated renders do not emit either callback.

## Interaction gates

Use `onWheelRequest` and `onTouchRequest` to allow or block interactions.
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pnpm add react-easy-crop
npm install react-easy-crop --save
```

Version 7 requires React 19.2 or newer. See [Migrate from v6 to v7](./migration-v7) when upgrading an existing application.

The cropper fills its parent with `position: absolute`, so wrap it in an element with a stable size and `position: relative`.

```tsx
Expand Down
72 changes: 72 additions & 0 deletions docs/docs/migration-v7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: Migrate from v6 to v7
---

# Migrate from v6 to v7

Version 7 replaces the class implementation with hooks and requires React 19.2 or newer.

The published package is built with React Compiler. Consumers do not need to configure the compiler to use the cropper.

```shell
pnpm add react@^19.2.0 react-dom@^19.2.0 react-easy-crop@^7
```

## Replace legacy refs

The component `ref` now points to the outer container. The class instance and its imperative fields and methods are no longer public.

Use `cropAreaRef` for the visible crop area and `mediaRef` for the image or video:

```tsx
import { useRef } from 'react'

const containerRef = useRef<HTMLDivElement>(null)
const cropAreaRef = useRef<HTMLDivElement>(null)
const mediaRef = useRef<HTMLImageElement | HTMLVideoElement>(null)

<Cropper
ref={containerRef}
cropAreaRef={cropAreaRef}
mediaRef={mediaRef}
// ...controlled props
/>
```

Replace the removed props as follows:

| Version 6 | Version 7 |
| ------------------------------ | ---------------------------- |
| Class-instance `ref` | `ref` to the outer container |
| `setCropperRef` | `cropAreaRef` |
| `setImageRef` or `setVideoRef` | `mediaRef` |
| `setMediaSize` | `onMediaSizeChange` |
| `setCropSize` | `onCropSizeChange` |

Refs accept callback refs and ref objects. `onMediaLoaded` remains available for load-specific work; use `onMediaSizeChange` when every measured size change matters.

## Callback timing

Callbacks are deduplicated in version 7:

- `onCropAreaChange` runs once for each committed crop calculation during an interaction or relevant controlled update.
- `onCropComplete` runs after media initialization, at interaction completion, or 250 ms after the final wheel or resize event.
- `onMediaSizeChange` and `onCropSizeChange` run only when their measured values change.
- Unrelated renders and equivalent values do not emit callbacks.
- Controlled crop corrections emit crop data only after the corrected value commits.

Do not depend on callbacks firing again because a callback prop or unrelated prop changed.

## Styles

Automatic CSS now uses a React-managed stylesheet resource. React hoists and deduplicates the style in the correct document, including iframe portals, and includes it during server rendering. When `nonce` is set, the cropper uses a nonce-bearing inline stylesheet so the prop also works with non-streaming server rendering. `disableAutomaticStylesInjection` remains available.

When automatic styles are disabled, import the package CSS:

```tsx
import 'react-easy-crop/react-easy-crop.css'
```

## TypeScript defaults

Props with runtime defaults are now optional in `CropperProps`. Existing controlled `crop` and `onCropChange` usage is unchanged.
66 changes: 33 additions & 33 deletions docs/docs/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,36 @@ title: Props

# Props

| Prop | Type | Required | Description |
| --- | --- | :-: | --- |
| `image` | `string` | | Image to crop. `image` or `video` is required. |
| `video` | `string \| Array<{ src: string; type?: string }>` | | Video to crop. `image` or `video` is required. |
| `crop` | `{ x: number; y: number }` | yes | Media position. `{ x: 0, y: 0 }` centers the media under the cropper. |
| `zoom` | `number` | | Zoom between `minZoom` and `maxZoom`. Defaults to `1`. |
| `rotation` | `number` | | Rotation in degrees. Defaults to `0`. |
| `aspect` | `number` | | Crop area ratio. Defaults to `4 / 3`. |
| `minZoom` | `number` | | Minimum zoom. Defaults to `1`. |
| `maxZoom` | `number` | | Maximum zoom. Defaults to `3`. |
| `zoomWithScroll` | `boolean` | | Enable scroll zoom. Defaults to `true`. |
| `cropShape` | `'rect' \| 'round'` | | Crop area shape. Defaults to `'rect'`. |
| `cropSize` | `{ width: number; height: number }` | | Fixed crop area size in pixels. Prefer `aspect` unless you really need fixed dimensions. |
| `showGrid` | `boolean` | | Show third-line grid. Defaults to `true`. |
| `roundCropAreaPixels` | `boolean` | | Round crop area dimensions to integer pixels. Defaults to `false`. |
| `zoomSpeed` | `number` | | Multiplies zoom changes. Defaults to `1`. |
| `objectFit` | `'contain' \| 'cover' \| 'horizontal-cover' \| 'vertical-cover'` | | Controls how the media fits the cropper. Defaults to `'contain'`. |
| `restrictPosition` | `boolean` | | Restrict media position to cropper boundaries. Useful when `zoom < 1`. |
| `initialCroppedAreaPercentages` | `{ width: number; height: number; x: number; y: number }` | | Restore a crop from a previous `croppedArea` value. Preferred over pixels. |
| `initialCroppedAreaPixels` | `{ width: number; height: number; x: number; y: number }` | | Restore a crop from a previous `croppedAreaPixels` value. |
| `transform` | `string` | | Custom CSS transform for the media. See [Custom transform](./advanced#custom-transform). |
| `style` | `{ containerStyle?: object; mediaStyle?: object; cropAreaStyle?: object }` | | Inline style overrides. |
| `classes` | `{ containerClassName?: string; mediaClassName?: string; cropAreaClassName?: string }` | | Custom class names. |
| `mediaProps` | `object` | | Props passed to the image or video element. |
| `cropperProps` | `object` | | Props passed to the cropper container. |
| `disableAutomaticStylesInjection` | `boolean` | | Disable automatic CSS injection. |
| `setCropperRef` | `(ref: React.RefObject<HTMLDivElement>) => void` | | Receives the cropper ref. |
| `setImageRef` | `(ref: React.RefObject<HTMLImageElement>) => void` | | Receives the image ref. |
| `setVideoRef` | `(ref: React.RefObject<HTMLVideoElement>) => void` | | Receives the video ref. |
| `setMediaSize` | `(size: MediaSize) => void` | | Exposes media size for advanced restore helpers. |
| `setCropSize` | `(size: Size) => void` | | Exposes crop size for advanced restore helpers. |
| `nonce` | `string` | | Nonce added to the injected style tag. |
| `keyboardStep` | `number` | | Pixels moved per arrow key press. Defaults to `1`. |
| Prop | Type | Required | Description |
| --------------------------------- | -------------------------------------------------------------------------------------- | :------: | ------------------------------------------------------------------------------------------------ |
| `ref` | `React.Ref<HTMLDivElement>` | | Ref to the outer cropper container. |
| `image` | `string` | | Image to crop. `image` or `video` is required. |
| `video` | `string \| Array<{ src: string; type?: string }>` | | Video to crop. `image` or `video` is required. |
| `crop` | `{ x: number; y: number }` | yes | Media position. `{ x: 0, y: 0 }` centers the media under the cropper. |
| `zoom` | `number` | | Zoom between `minZoom` and `maxZoom`. Defaults to `1`. |
| `rotation` | `number` | | Rotation in degrees. Defaults to `0`. |
| `aspect` | `number` | | Crop area ratio. Defaults to `4 / 3`. |
| `minZoom` | `number` | | Minimum zoom. Defaults to `1`. |
| `maxZoom` | `number` | | Maximum zoom. Defaults to `3`. |
| `zoomWithScroll` | `boolean` | | Enable scroll zoom. Defaults to `true`. |
| `cropShape` | `'rect' \| 'round'` | | Crop area shape. Defaults to `'rect'`. |
| `cropSize` | `{ width: number; height: number }` | | Fixed crop area size in pixels. Prefer `aspect` unless you really need fixed dimensions. |
| `showGrid` | `boolean` | | Show third-line grid. Defaults to `true`. |
| `roundCropAreaPixels` | `boolean` | | Round crop area dimensions to integer pixels. Defaults to `false`. |
| `zoomSpeed` | `number` | | Multiplies zoom changes. Defaults to `1`. |
| `objectFit` | `'contain' \| 'cover' \| 'horizontal-cover' \| 'vertical-cover'` | | Controls how the media fits the cropper. Defaults to `'contain'`. |
| `restrictPosition` | `boolean` | | Restrict media position to cropper boundaries. Useful when `zoom < 1`. |
| `initialCroppedAreaPercentages` | `{ width: number; height: number; x: number; y: number }` | | Restore a crop from a previous `croppedArea` value. Preferred over pixels. |
| `initialCroppedAreaPixels` | `{ width: number; height: number; x: number; y: number }` | | Restore a crop from a previous `croppedAreaPixels` value. |
| `transform` | `string` | | Custom CSS transform for the media. See [Custom transform](./advanced#custom-transform). |
| `style` | `{ containerStyle?: object; mediaStyle?: object; cropAreaStyle?: object }` | | Inline style overrides. |
| `classes` | `{ containerClassName?: string; mediaClassName?: string; cropAreaClassName?: string }` | | Custom class names. |
| `mediaProps` | `object` | | Props passed to the image or video element. |
| `cropperProps` | `object` | | Props passed to the cropper container. |
| `cropAreaRef` | `React.Ref<HTMLDivElement>` | | Ref to the visible crop area. |
| `mediaRef` | `React.Ref<HTMLImageElement \| HTMLVideoElement>` | | Ref to the rendered image or video. |
| `onMediaSizeChange` | `(size: MediaSize) => void` | | Called when the measured media size changes. |
| `onCropSizeChange` | `(size: Size) => void` | | Called when the crop area size changes. |
| `disableAutomaticStylesInjection` | `boolean` | | Disable the React-managed stylesheet resource. |
| `nonce` | `string` | | Content Security Policy nonce. When set, automatic styles use a nonce-bearing inline stylesheet. |
| `keyboardStep` | `number` | | Pixels moved per arrow key press. Defaults to `1`. |
10 changes: 8 additions & 2 deletions docs/docs/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ title: Styling

# Styling

`react-easy-crop` injects its required CSS automatically.
`react-easy-crop` renders its CSS automatically as a React-managed stylesheet resource. React hoists it into the document head and deduplicates it. A cropper rendered through a portal into an iframe uses that iframe's document.

If you disable automatic injection, import the CSS yourself:
Pass `nonce` when your Content Security Policy requires a nonce for inline styles. This switches to a nonce-bearing inline stylesheet so it also works with non-streaming server rendering:

```tsx
<Cropper nonce={cspNonce} />
```

If you disable the managed stylesheet, import the CSS yourself:

```tsx
import 'react-easy-crop/react-easy-crop.css'
Expand Down
15 changes: 9 additions & 6 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
"author": "ValentinH",
"license": "MIT",
"scripts": {
"build": "docusaurus build",
"build": "pnpm build:library && pnpm build:site",
"build:library": "pnpm --dir .. build",
"build:site": "docusaurus build",
"deploy": "docusaurus deploy",
"prestart": "pnpm build:library",
"serve": "docusaurus serve",
"start": "docusaurus start"
},
Expand All @@ -17,16 +20,16 @@
"@mdx-js/react": "^3.1.1",
"clsx": "^2.1.1",
"prism-react-renderer": "^2.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-easy-crop": "5.5.6"
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-easy-crop": "link:../dist"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.10.1",
"@docusaurus/tsconfig": "3.10.1",
"@docusaurus/types": "3.10.1",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"typescript": "^5.8.3"
}
}
Loading