mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 03:45:06 +03:00
fix(studio): beef up next/image + next/legacy/image compat shims
Both shims still degrade to a plain <img> (Vite has no Next image
optimizer), but the prop surface now matches the real APIs so consumer
code compiles and behavioural defaults line up with Next.
Added across both:
- forwardRef so callers can take an HTMLImageElement ref.
- \`loader\` prop that, when provided, is called with { src, width,
quality } to build the final src URL.
- \`onLoadingComplete\` callback fired on image load, plus an effect
that fires it on mount when the image is already cached.
- \`unoptimized\` accepted-and-dropped (no optimizer to disable).
- \`sizes\`, \`quality\`, \`placeholder\`, \`blurDataURL\` typed and accepted.
- \`priority\` now drives loading='eager' + fetchPriority='high';
\`loading\` honoured when explicitly set, otherwise defaults to 'lazy'
matching Next.
- Object-form src ({ src, width, height }) supported in addition to
string.
next/image-specific: \`fill\` flag still stretches the image to its
container.
next/legacy/image-specific: \`layout\` / \`objectFit\` / \`objectPosition\`
preserved.
This commit is contained in:
1 parent
e5e9711660
commit
8db082fc86
2 files changed
+209
-35
No files matched your search
@@ -1,28 +1,120 @@
|
||||
import type { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
type CSSProperties,
|
||||
type ForwardedRef,
|
||||
type SyntheticEvent,
|
||||
} from 'react'
|
||||
|
||||
type ImageProps = Omit<ComponentPropsWithoutRef<'img'>, 'src' | 'alt'> & {
|
||||
src: string
|
||||
// Next/Image is a smart wrapper around `<img>` that handles automatic
|
||||
// resizing, lazy loading, blur placeholders, and a CDN loader. Under
|
||||
// Vite we don't run the Next image optimizer, so this shim degrades to
|
||||
// a plain `<img>` while preserving the prop surface so consumer code
|
||||
// compiles without modification.
|
||||
|
||||
type ImageLoaderProps = { src: string; width: number; quality?: number }
|
||||
type ImageLoader = (props: ImageLoaderProps) => string
|
||||
|
||||
interface ImageProps extends Omit<ComponentPropsWithoutRef<'img'>, 'src' | 'alt' | 'loading'> {
|
||||
src: string | { src: string; width?: number; height?: number }
|
||||
alt: string
|
||||
width?: number | `${number}`
|
||||
height?: number | `${number}`
|
||||
fill?: boolean
|
||||
sizes?: string
|
||||
priority?: boolean
|
||||
quality?: number
|
||||
placeholder?: 'blur' | 'empty'
|
||||
loading?: 'lazy' | 'eager'
|
||||
quality?: number | `${number}`
|
||||
loader?: ImageLoader
|
||||
placeholder?: 'blur' | 'empty' | `data:image/${string}`
|
||||
blurDataURL?: string
|
||||
unoptimized?: boolean
|
||||
onLoadingComplete?: (img: HTMLImageElement) => void
|
||||
}
|
||||
|
||||
export default function Image({
|
||||
fill,
|
||||
sizes: _sizes,
|
||||
priority: _priority,
|
||||
quality: _quality,
|
||||
placeholder: _placeholder,
|
||||
blurDataURL: _blurDataURL,
|
||||
style,
|
||||
...rest
|
||||
}: ImageProps) {
|
||||
function resolveSrc(
|
||||
src: ImageProps['src'],
|
||||
width?: ImageProps['width'],
|
||||
quality?: ImageProps['quality'],
|
||||
loader?: ImageLoader
|
||||
): string {
|
||||
const raw = typeof src === 'string' ? src : src.src
|
||||
if (!loader) return raw
|
||||
return loader({
|
||||
src: raw,
|
||||
width: typeof width === 'number' ? width : Number(width ?? 0),
|
||||
quality: quality !== undefined ? Number(quality) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const Image = forwardRef(function Image(
|
||||
{
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
fill,
|
||||
sizes,
|
||||
priority,
|
||||
loading,
|
||||
quality,
|
||||
loader,
|
||||
placeholder: _placeholder,
|
||||
blurDataURL: _blurDataURL,
|
||||
unoptimized: _unoptimized,
|
||||
onLoad,
|
||||
onLoadingComplete,
|
||||
style,
|
||||
...rest
|
||||
}: ImageProps,
|
||||
forwardedRef: ForwardedRef<HTMLImageElement>
|
||||
) {
|
||||
const innerRef = useRef<HTMLImageElement | null>(null)
|
||||
|
||||
// Mirror Next's onLoadingComplete by firing once when the image has
|
||||
// finished decoding. Next deduplicates against re-fires; we approximate
|
||||
// by firing on the load event (the practical observable difference is
|
||||
// negligible for our bundle).
|
||||
const handleLoad = (e: SyntheticEvent<HTMLImageElement>) => {
|
||||
onLoad?.(e)
|
||||
if (onLoadingComplete && e.currentTarget) {
|
||||
onLoadingComplete(e.currentTarget)
|
||||
}
|
||||
}
|
||||
|
||||
// If onLoadingComplete is provided and the image is already cached
|
||||
// (loaded synchronously before our handler attaches), fire it on
|
||||
// mount so the contract holds.
|
||||
useEffect(() => {
|
||||
const img = innerRef.current
|
||||
if (!onLoadingComplete || !img) return
|
||||
if (img.complete && img.naturalWidth > 0) onLoadingComplete(img)
|
||||
}, [onLoadingComplete])
|
||||
|
||||
const finalStyle: CSSProperties | undefined = fill
|
||||
? { position: 'absolute', inset: 0, width: '100%', height: '100%', ...style }
|
||||
: style
|
||||
return <img {...rest} style={finalStyle} />
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
ref={(node) => {
|
||||
innerRef.current = node
|
||||
if (typeof forwardedRef === 'function') forwardedRef(node)
|
||||
else if (forwardedRef) forwardedRef.current = node
|
||||
}}
|
||||
src={resolveSrc(src, width, quality, loader)}
|
||||
width={fill ? undefined : width}
|
||||
height={fill ? undefined : height}
|
||||
sizes={sizes}
|
||||
// Match Next's defaults: lazy unless priority/loading says otherwise.
|
||||
loading={loading ?? (priority ? 'eager' : 'lazy')}
|
||||
fetchPriority={priority ? 'high' : rest.fetchPriority}
|
||||
style={finalStyle}
|
||||
onLoad={handleLoad}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export default Image
|
||||
@@ -1,33 +1,96 @@
|
||||
import type { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
type CSSProperties,
|
||||
type ForwardedRef,
|
||||
type SyntheticEvent,
|
||||
} from 'react'
|
||||
|
||||
// `next/legacy/image` is the pre-Next-13 Image API. Functionally similar
|
||||
// to `next/image` but with `layout` and `objectFit`/`objectPosition`
|
||||
// props instead of `fill` + style. Same shim approach: degrade to a
|
||||
// plain <img> with the prop surface preserved.
|
||||
|
||||
type ImageLoaderProps = { src: string; width: number; quality?: number }
|
||||
type ImageLoader = (props: ImageLoaderProps) => string
|
||||
|
||||
type Layout = 'fill' | 'fixed' | 'intrinsic' | 'responsive'
|
||||
type ObjectFit = CSSProperties['objectFit']
|
||||
|
||||
type ImageProps = Omit<ComponentPropsWithoutRef<'img'>, 'src' | 'alt'> & {
|
||||
src: string
|
||||
interface ImageProps extends Omit<ComponentPropsWithoutRef<'img'>, 'src' | 'alt' | 'loading'> {
|
||||
src: string | { src: string }
|
||||
alt: string
|
||||
width?: number | `${number}`
|
||||
height?: number | `${number}`
|
||||
layout?: Layout
|
||||
objectFit?: ObjectFit
|
||||
objectPosition?: CSSProperties['objectPosition']
|
||||
priority?: boolean
|
||||
quality?: number
|
||||
loading?: 'lazy' | 'eager'
|
||||
quality?: number | `${number}`
|
||||
loader?: ImageLoader
|
||||
placeholder?: 'blur' | 'empty'
|
||||
blurDataURL?: string
|
||||
unoptimized?: boolean
|
||||
sizes?: string
|
||||
onLoadingComplete?: (img: HTMLImageElement) => void
|
||||
}
|
||||
|
||||
export default function Image({
|
||||
layout,
|
||||
objectFit,
|
||||
objectPosition,
|
||||
priority: _priority,
|
||||
quality: _quality,
|
||||
placeholder: _placeholder,
|
||||
blurDataURL: _blurDataURL,
|
||||
sizes: _sizes,
|
||||
style,
|
||||
...rest
|
||||
}: ImageProps) {
|
||||
function resolveSrc(
|
||||
src: ImageProps['src'],
|
||||
width?: ImageProps['width'],
|
||||
quality?: ImageProps['quality'],
|
||||
loader?: ImageLoader
|
||||
): string {
|
||||
const raw = typeof src === 'string' ? src : src.src
|
||||
if (!loader) return raw
|
||||
return loader({
|
||||
src: raw,
|
||||
width: typeof width === 'number' ? width : Number(width ?? 0),
|
||||
quality: quality !== undefined ? Number(quality) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const Image = forwardRef(function Image(
|
||||
{
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
layout,
|
||||
objectFit,
|
||||
objectPosition,
|
||||
priority,
|
||||
loading,
|
||||
quality,
|
||||
loader,
|
||||
placeholder: _placeholder,
|
||||
blurDataURL: _blurDataURL,
|
||||
unoptimized: _unoptimized,
|
||||
sizes,
|
||||
onLoad,
|
||||
onLoadingComplete,
|
||||
style,
|
||||
...rest
|
||||
}: ImageProps,
|
||||
forwardedRef: ForwardedRef<HTMLImageElement>
|
||||
) {
|
||||
const innerRef = useRef<HTMLImageElement | null>(null)
|
||||
|
||||
const handleLoad = (e: SyntheticEvent<HTMLImageElement>) => {
|
||||
onLoad?.(e)
|
||||
if (onLoadingComplete && e.currentTarget) {
|
||||
onLoadingComplete(e.currentTarget)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const img = innerRef.current
|
||||
if (!onLoadingComplete || !img) return
|
||||
if (img.complete && img.naturalWidth > 0) onLoadingComplete(img)
|
||||
}, [onLoadingComplete])
|
||||
|
||||
const layoutStyle: CSSProperties | undefined =
|
||||
layout === 'fill'
|
||||
? { position: 'absolute', inset: 0, width: '100%', height: '100%' }
|
||||
@@ -40,5 +103,24 @@ export default function Image({
|
||||
? { ...layoutStyle, objectFit, objectPosition, ...style }
|
||||
: undefined
|
||||
|
||||
return <img {...rest} style={finalStyle} />
|
||||
}
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
ref={(node) => {
|
||||
innerRef.current = node
|
||||
if (typeof forwardedRef === 'function') forwardedRef(node)
|
||||
else if (forwardedRef) forwardedRef.current = node
|
||||
}}
|
||||
src={resolveSrc(src, width, quality, loader)}
|
||||
width={layout === 'fill' ? undefined : width}
|
||||
height={layout === 'fill' ? undefined : height}
|
||||
sizes={sizes}
|
||||
loading={loading ?? (priority ? 'eager' : 'lazy')}
|
||||
fetchPriority={priority ? 'high' : rest.fetchPriority}
|
||||
style={finalStyle}
|
||||
onLoad={handleLoad}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export default Image
|
||||
Reference in new issue
Block a user