diff --git a/package.json b/package.json index 824211fa714..c9ca40ad286 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "check:format": "prettier . --check", "check:lint": "turbo run lint --continue -- --quiet", "check:react-exhaustive-deps": "turbo run lint --continue -- --quiet --rule 'react-hooks/exhaustive-deps: [error, {additionalHooks: \"(useMemoObservable|useObservableCallback)\"}]'", - "check:react-compiler": "eslint --no-inline-config --no-eslintrc --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --rule 'react-compiler/react-compiler: [warn]' --ignore-path .eslintignore.react-compiler --max-warnings 35 .", + "check:react-compiler": "eslint --no-inline-config --no-eslintrc --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --rule 'react-compiler/react-compiler: [warn]' --ignore-path .eslintignore.react-compiler --max-warnings 34 .", "check:test": "run-s test -- --silent", "check:types": "tsc && turbo run check:types --filter='./packages/*' --filter='./packages/@sanity/*'", "chore:format:fix": "prettier --cache --write .", diff --git a/packages/sanity/src/core/form/inputs/files/ImageToolInput/imagetool/Resize.tsx b/packages/sanity/src/core/form/inputs/files/ImageToolInput/imagetool/Resize.tsx index 8ea04c8ec96..59acc8fd48d 100644 --- a/packages/sanity/src/core/form/inputs/files/ImageToolInput/imagetool/Resize.tsx +++ b/packages/sanity/src/core/form/inputs/files/ImageToolInput/imagetool/Resize.tsx @@ -1,5 +1,4 @@ -/* eslint-disable @typescript-eslint/no-shadow */ -import {type ReactNode, useCallback, useEffect, useState} from 'react' +import {type ReactNode, useLayoutEffect, useRef, useState} from 'react' export interface ResizeProps { image: HTMLImageElement @@ -10,41 +9,57 @@ export interface ResizeProps { export function Resize(props: ResizeProps): any { const {image, maxHeight, maxWidth, children} = props + const canvasRef = useRef(null) + const [ready, setReady] = useState(false) - const [canvas] = useState(() => { - const canvasElement = document.createElement('canvas') - canvasElement.style.display = 'none' - return canvasElement - }) - useEffect(() => { - document.body.appendChild(canvas) - return () => { - document.body.removeChild(canvas) + /** + * The useLayoutEffect is used here intentionally. + * Since the first render doesn't have a canvas element yet we return `null` instead of calling `children` so that `ImageTool` don't have to deal with + * the initial render not having a canvas element. + * Now, the flow is that first ImageTool will render a loading state, then it will render and expect it to have a canvas that + * renders the provided image. + * If we use `useEffect` there will be a flash where just finished rendering loading, + * then it will render with nothing, causing a jump, + * and finally it renders the image inside the canvas. + * By using `useLayoutEffect` we ensure that the intermediary state where there is no canvas doesn't paint in the browser, + * React blocks it, runs render again, this time we have a canvas element that got setup inside the effect and assigned to the ref, + * and then we render the image inside the canvas. + * No flash, no jumps, just a smooth transition from loading to image. + */ + useLayoutEffect(() => { + if (!canvasRef.current) { + const canvasElement = document.createElement('canvas') + canvasElement.style.display = 'none' + canvasRef.current = canvasElement + setReady(true) } - }, [canvas]) - const resize = useCallback( - (image: HTMLImageElement, maxHeight: number, maxWidth: number) => { - const ratio = image.width / image.height - const width = Math.min(image.width, maxWidth) - const height = Math.min(image.height, maxHeight) + const ratio = image.width / image.height + const width = Math.min(image.width, maxWidth) + const height = Math.min(image.height, maxHeight) + + const landscape = image.width > image.height + const targetWidth = landscape ? width : height * ratio + const targetHeight = landscape ? width / ratio : height - const landscape = image.width > image.height - const targetWidth = landscape ? width : height * ratio - const targetHeight = landscape ? width / ratio : height + canvasRef.current.width = targetWidth + canvasRef.current.height = targetHeight - canvas.width = targetWidth - canvas.height = targetHeight + const ctx = canvasRef.current.getContext('2d') + if (ctx) { + ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, targetWidth, targetHeight) + } - const ctx = canvas.getContext('2d') - if (ctx) { - ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, targetWidth, targetHeight) - } + const node = canvasRef.current + document.body.appendChild(node) + return () => { + document.body.removeChild(node) + } + }, [image, maxHeight, maxWidth]) - return canvas - }, - [canvas], - ) + if (!canvasRef.current || !ready) { + return null + } - return children(resize(image, maxHeight, maxWidth)) + return children(canvasRef.current) }