From 0e4c2eda45446f0ff0dccbb80bbe03c87d785336 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Sat, 26 Mar 2022 00:24:49 +0100 Subject: [PATCH 01/17] fix(gatsby-plugin-image): fix image flickers --- packages/gatsby-plugin-image/package.json | 9 +- .../__tests__/gatsby-image.browser.tsx | 81 +--- .../src/components/gatsby-image.browser.tsx | 411 +++++++----------- .../src/components/gatsby-image.server.tsx | 27 +- .../src/components/hooks.ts | 142 +----- .../src/components/intersection-observer.ts | 23 +- .../src/components/later-hydrator.tsx | 11 - .../src/components/layout-wrapper.tsx | 11 +- .../src/components/lazy-hydrate.tsx | 260 +++++++---- .../src/components/main-image.tsx | 24 +- .../src/components/picture.tsx | 75 ++-- .../src/components/placeholder.tsx | 1 + .../gatsby-plugin-image/src/gatsby-node.ts | 6 +- packages/gatsby-plugin-image/src/global.d.ts | 7 - packages/gatsby-plugin-image/src/global.ts | 12 + .../gatsby-plugin-image/src/image-utils.ts | 8 +- .../gatsby-plugin-image/src/index.browser.ts | 2 +- packages/gatsby-plugin-image/src/index.ts | 1 + .../gatsby-plugin-image/src/resolver-utils.ts | 8 +- packages/gatsby-plugin-image/tsconfig.json | 2 +- 20 files changed, 451 insertions(+), 670 deletions(-) delete mode 100644 packages/gatsby-plugin-image/src/components/later-hydrator.tsx delete mode 100644 packages/gatsby-plugin-image/src/global.d.ts create mode 100644 packages/gatsby-plugin-image/src/global.ts diff --git a/packages/gatsby-plugin-image/package.json b/packages/gatsby-plugin-image/package.json index 9faa84e91c379..6f0e076df8364 100644 --- a/packages/gatsby-plugin-image/package.json +++ b/packages/gatsby-plugin-image/package.json @@ -6,9 +6,9 @@ "build:gatsby-node": "tsc --jsx react --downlevelIteration true --skipLibCheck true --esModuleInterop true --outDir dist/ src/gatsby-node.ts src/babel-plugin-parse-static-images.ts src/resolver-utils.ts src/types.d.ts -d --declarationDir dist/src", "build:gatsby-ssr": "microbundle -i src/gatsby-ssr.tsx -f cjs -o ./[name].js --no-pkg-main --jsx React.createElement --no-compress --external=common-tags,react --no-sourcemap", "build:server": "microbundle -f cjs,es --jsx React.createElement --define SERVER=true", - "build:browser": "microbundle -i src/index.browser.ts -f cjs,modern,es --jsx React.createElement -o dist/gatsby-image.browser --define SERVER=false", - "prepare": "yarn build", - "watch": "run-p watch:*", + "build:browser": "microbundle -i src/index.browser.ts -f cjs,modern --jsx React.createElement -o dist/gatsby-image.browser --define SERVER=false", + "prepare": "build", + "watch": "npm-run-all -s clean -p watch:*", "watch:gatsby-node": "yarn build:gatsby-node --watch", "watch:gatsby-ssr": "yarn build:gatsby-ssr watch", "watch:server": "yarn build:server --no-compress watch", @@ -27,7 +27,6 @@ "esmodule": "dist/gatsby-image.modern.js", "browser": { "./dist/gatsby-image.js": "./dist/gatsby-image.browser.js", - "./dist/gatsby-image.module.js": "./dist/gatsby-image.browser.module.js", "./dist/gatsby-image.modern.js": "./dist/gatsby-image.browser.modern.js" }, "files": [ @@ -94,4 +93,4 @@ }, "author": "Matt Kane ", "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx b/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx index ac74a05b8fe42..25fbcde9c147d 100644 --- a/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx +++ b/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /** * @jest-environment jsdom */ @@ -15,7 +16,20 @@ jest.mock( strs.join(``) ) -// test +let count = 0 +function generateImage(): IGatsbyImageData { + return { + width: 100, + height: 100, + layout: `fullWidth`, + images: { + fallback: { src: `some-src-fallback-${count++}.jpg`, sizes: `192x192` }, + }, + placeholder: { sources: [] }, + + backgroundColor: `red`, + } +} describe(`GatsbyImage browser`, () => { let beforeHydrationContent: HTMLDivElement @@ -27,18 +41,9 @@ describe(`GatsbyImage browser`, () => { console.error = jest.fn() global.SERVER = true global.GATSBY___IMAGE = true - global.HAS_REACT_18 = false GatsbyImage = require(`../gatsby-image.browser`).GatsbyImage - image = { - width: 100, - height: 100, - layout: `fullWidth`, - images: { fallback: { src: `some-src-fallback.jpg`, sizes: `192x192` } }, - placeholder: { sources: [] }, - - backgroundColor: `red`, - } + image = generateImage() beforeHydrationContent = document.createElement(`div`) beforeHydrationContent.innerHTML = ` @@ -81,7 +86,6 @@ describe(`GatsbyImage browser`, () => { jest.clearAllMocks() global.SERVER = undefined global.GATSBY___IMAGE = undefined - global.HAS_REACT_18 = undefined process.env.NODE_ENV = `test` }) @@ -148,6 +152,8 @@ describe(`GatsbyImage browser`, () => { expect(placeholder).toBeDefined() expect(mainImage).toBeDefined() + expect(placeholder.style.opacity).toBe(`1`) + expect(mainImage.style.opacity).toBe(`0`) }) it(`relies on native lazy loading when the SSR element exists and that the browser supports native lazy loading`, async () => { @@ -157,11 +163,10 @@ describe(`GatsbyImage browser`, () => { // In this scenario, // hasSSRHtml is true and resolved through "beforeHydrationContent" and hydrate: true ;(hooks as any).hasNativeLazyLoadSupport = (): boolean => true - ;(hooks as any).storeImageloaded = jest.fn() const { container } = render( { img?.dispatchEvent(new Event(`load`)) - expect(onStartLoadSpy).toBeCalledWith({ wasCached: false }) + expect(onStartLoadSpy).toBeCalledWith({ wasCached: true }) expect(onLoadSpy).toBeCalled() - expect(hooks.storeImageloaded).toBeCalledWith( - `{"fallback":{"src":"some-src-fallback.jpg","sizes":"192x192"}}` - ) - }) - - it(`relies on intersection observer when the SSR element is not resolved`, async () => { - ;(hooks as any).hasNativeLazyLoadSupport = (): boolean => true - const onStartLoadSpy = jest.fn() - let GatsbyImage - jest.isolateModules(() => { - GatsbyImage = require(`../gatsby-image.browser`).GatsbyImage - }) - - const { container } = render( - - ) - - await waitFor(() => container.querySelector(`[data-main-image=""]`)) - - expect(onStartLoadSpy).toBeCalledWith({ wasCached: false }) - }) - - it(`relies on intersection observer when browser does not support lazy loading`, async () => { - ;(hooks as any).hasNativeLazyLoadSupport = (): boolean => false - const onStartLoadSpy = jest.fn() - let GatsbyImage - jest.isolateModules(() => { - GatsbyImage = require(`../gatsby-image.browser`).GatsbyImage - }) - - const { container } = render( - , - { container: beforeHydrationContent, hydrate: true } - ) - - await waitFor(() => container.querySelector(`[data-main-image=""]`)) - - expect(onStartLoadSpy).toBeCalledWith({ wasCached: false }) }) }) diff --git a/packages/gatsby-plugin-image/src/components/gatsby-image.browser.tsx b/packages/gatsby-plugin-image/src/components/gatsby-image.browser.tsx index 625c39fd01c12..de0f809a680b6 100644 --- a/packages/gatsby-plugin-image/src/components/gatsby-image.browser.tsx +++ b/packages/gatsby-plugin-image/src/components/gatsby-image.browser.tsx @@ -1,55 +1,30 @@ -/* global HAS_REACT_18 */ -/* eslint-disable no-unused-expressions */ -import React, { - Component, +import { + createElement, + memo, + useMemo, + useEffect, + useLayoutEffect, + useRef, +} from "react" +import { getWrapperProps, gatsbyImageIsInstalled } from "./hooks" +import { getSizer } from "./layout-wrapper" +import { propTypes } from "./gatsby-image.server" +import type { + FC, ElementType, - createRef, - MutableRefObject, FunctionComponent, ImgHTMLAttributes, - RefObject, CSSProperties, + ReactEventHandler, } from "react" -import { - getWrapperProps, - hasNativeLazyLoadSupport, - storeImageloaded, - hasImageLoaded, - gatsbyImageIsInstalled, -} from "./hooks" -import { PlaceholderProps } from "./placeholder" -import { MainImageProps } from "./main-image" -import { Layout } from "../image-utils" -import { getSizer } from "./layout-wrapper" -import { propTypes } from "./gatsby-image.server" -import { Unobserver } from "./intersection-observer" -import type { Root } from "react-dom/client" - -let reactRender -if (HAS_REACT_18) { - const reactDomClient = require(`react-dom/client`) - reactRender = ( - Component: React.ReactChild | Iterable, - el: ReactDOM.Container, - root: Root - ): Root => { - if (!root) { - root = reactDomClient.createRoot(el) - } - - root.render(Component) +import type { renderImageToString } from "./lazy-hydrate" +import type { PlaceholderProps } from "./placeholder" +import type { MainImageProps } from "./main-image" +import type { Layout } from "../image-utils" - return root - } -} else { - const reactDomClient = require(`react-dom`) - reactRender = ( - Component: React.ReactChild | Iterable, - el: ReactDOM.Container - ): void => { - reactDomClient.render(Component, el) - } -} +const imageCache = new Set() +let renderImageToStringPromise +let renderImage: typeof renderImageToString | undefined // eslint-disable-next-line @typescript-eslint/naming-convention export interface GatsbyImageProps @@ -67,9 +42,9 @@ export interface GatsbyImageProps backgroundColor?: string objectFit?: CSSProperties["objectFit"] objectPosition?: CSSProperties["objectPosition"] - onLoad?: () => void - onError?: () => void - onStartLoad?: (props: { wasCached?: boolean }) => void + onLoad?: (props: { wasCached: boolean }) => void + onError?: ReactEventHandler + onStartLoad?: (props: { wasCached: boolean }) => void } export interface IGatsbyImageData { @@ -81,232 +56,163 @@ export interface IGatsbyImageData { placeholder?: Pick } -class GatsbyImageHydrator extends Component< - GatsbyImageProps, - { isLoading: boolean; isLoaded: boolean } -> { - root: RefObject = createRef< - HTMLImageElement | undefined - >() - hydrated: MutableRefObject = { current: false } - forceRender: MutableRefObject = { - // In dev we use render not hydrate, to avoid hydration warnings - current: process.env.NODE_ENV === `development`, - } - lazyHydrator: () => void | null = null - ref = createRef() - unobserveRef: Unobserver - reactRootRef: MutableRefObject = createRef() - - constructor(props) { - super(props) - - this.state = { - isLoading: hasNativeLazyLoadSupport(), - isLoaded: false, - } +const GatsbyImageHydrator: FC = function GatsbyImageHydrator( + props +) { + const { width, height, layout } = props.image + const { + style: wStyle, + className: wClass, + ...wrapperProps + } = getWrapperProps(width, height, layout) + const root = useRef() + const cacheKey = useMemo( + () => JSON.stringify(props.image.images), + [props.image.images] + ) + + let className = props.className + // Preact uses class instead of className so we need to check for both + if (props.class) { + className = props.class } - _lazyHydrate(props, state): Promise { - const hasSSRHtml = this.root.current.querySelector( - `[data-gatsby-image-ssr]` - ) - // On first server hydration do nothing - if (hasNativeLazyLoadSupport() && hasSSRHtml && !this.hydrated.current) { - this.hydrated.current = true - return Promise.resolve() - } + const sizer = getSizer(layout, width, height) - return import(`./lazy-hydrate`).then(({ lazyHydrate }) => { - const cacheKey = JSON.stringify(this.props.image.images) - this.lazyHydrator = lazyHydrate( - { - image: props.image.images, - isLoading: state.isLoading || hasImageLoaded(cacheKey), - isLoaded: state.isLoaded || hasImageLoaded(cacheKey), - toggleIsLoaded: () => { - props.onLoad?.() + useEffect(() => { + if (!renderImageToStringPromise) { + renderImageToStringPromise = import(`./lazy-hydrate`).then( + ({ renderImageToString, swapPlaceholderImage }) => { + renderImage = renderImageToString - this.setState({ - isLoaded: true, - }) - }, - ref: this.ref, - ...props, - }, - this.root, - this.hydrated, - this.forceRender, - this.reactRootRef + return { + renderImageToString, + swapPlaceholderImage, + } + } ) - }) - } + } - /** - * Choose if setupIntersectionObserver should use the image cache or not. - */ - _setupIntersectionObserver(useCache = true): void { - import(`./intersection-observer`).then(({ createIntersectionObserver }) => { - const intersectionObserver = createIntersectionObserver(() => { - if (this.root.current) { - const cacheKey = JSON.stringify(this.props.image.images) - this.props.onStartLoad?.({ - wasCached: useCache && hasImageLoaded(cacheKey), + // The plugin image component is a bit special where if it's server-side rendered, we add extra script tags to support lazy-loading without + // In this case we stop hydration but fire the correct events. + const ssrImage = root.current.querySelector( + `[data-gatsby-image-ssr]` + ) as HTMLImageElement + if (ssrImage) { + if (ssrImage.complete) { + // Trigger onStartload and onLoad events + props?.onStartLoad?.({ + wasCached: true, + }) + props?.onLoad?.({ + wasCached: true, + }) + } else { + document.addEventListener(`load`, function onLoad() { + document.removeEventListener(`load`, onLoad) + + props?.onStartLoad?.({ + wasCached: true, }) - this.setState({ - isLoading: true, - isLoaded: useCache && hasImageLoaded(cacheKey), + props?.onLoad?.({ + wasCached: true, }) - } - }) - - if (this.root.current) { - this.unobserveRef = intersectionObserver(this.root) - } - }) - } - - shouldComponentUpdate(nextProps, nextState): boolean { - let hasChanged = false - if (!this.state.isLoading && nextState.isLoading && !nextState.isLoaded) { - // Props have changed between SSR and hydration, so we need to force render instead of hydrate - this.forceRender.current = true - } - // this check mostly means people do not have the correct ref checks in place, we want to reset some state to suppport loading effects - if (this.props.image.images !== nextProps.image.images) { - // reset state, we'll rely on intersection observer to reload - if (this.unobserveRef) { - // unregister intersectionObserver - this.unobserveRef() - - // // on unmount, make sure we cleanup - if (this.hydrated.current && this.lazyHydrator) { - this.reactRootRef.current = reactRender( - null, - this.root.current, - this.reactRootRef.current - ) - } + }) } - this.setState( - { - isLoading: false, - isLoaded: false, - }, - () => { - this._setupIntersectionObserver(false) - } - ) + imageCache.add(cacheKey) - hasChanged = true + return } - if (this.root.current && !hasChanged) { - this._lazyHydrate(nextProps, nextState) + if (renderImage && imageCache.has(cacheKey)) { + return } - return false - } - - componentDidMount(): void { - if (this.root.current) { - const ssrElement = this.root.current.querySelector( - `[data-gatsby-image-ssr]` - ) as HTMLImageElement - const cacheKey = JSON.stringify(this.props.image.images) - - // when SSR and native lazyload is supported we'll do nothing ;) - if ( - hasNativeLazyLoadSupport() && - ssrElement && - gatsbyImageIsInstalled() - ) { - this.props.onStartLoad?.({ wasCached: false }) - - // When the image is already loaded before we have hydrated, we trigger onLoad and cache the item - if (ssrElement.complete) { - this.props.onLoad?.() - storeImageloaded(cacheKey) - } else { - // We need the current class context (this) inside our named onLoad function - // The named function is necessary to easily remove the listener afterward. - // eslint-disable-next-line @typescript-eslint/no-this-alias - const _this = this - // add an onLoad to the image - ssrElement.addEventListener(`load`, function onLoad() { - ssrElement.removeEventListener(`load`, onLoad) - - _this.props.onLoad?.() - storeImageloaded(cacheKey) + let animationFrame + let cleanupCallback + renderImageToStringPromise.then( + ({ renderImageToString, swapPlaceholderImage }) => { + root.current.innerHTML = renderImageToString({ + image: props.image.images, + isLoading: true, + isLoaded: imageCache.has(cacheKey), + ...props, + }) + + if (!imageCache.has(cacheKey)) { + animationFrame = requestAnimationFrame(() => { + if (root.current) { + cleanupCallback = swapPlaceholderImage( + root.current, + cacheKey, + imageCache, + props.style, + props.onStartLoad, + props.onLoad, + props.onError + ) + } }) } - - return } + ) - // Fallback to custom lazy loading (intersection observer) - this._setupIntersectionObserver(true) - } - } - - componentWillUnmount(): void { - // Cleanup when onmount happens - if (this.unobserveRef) { - // unregister intersectionObserver - this.unobserveRef() - - // on unmount, make sure we cleanup - if (this.hydrated.current && this.lazyHydrator) { - this.lazyHydrator() + // eslint-disable-next-line consistent-return + return (): void => { + if (animationFrame) { + cancelAnimationFrame(animationFrame) + } + if (cleanupCallback) { + cleanupCallback() } } + }, [props.image.images]) + + // We need to run this effect before browser has paint to make sure our html is set so no flickering happens + // + useLayoutEffect(() => { + if (imageCache.has(cacheKey) && renderImage) { + root.current.innerHTML = renderImage({ + image: props.image.images, + isLoading: imageCache.has(cacheKey), + isLoaded: imageCache.has(cacheKey), + ...props, + }) - return - } - - render(): JSX.Element { - const Type = this.props.as || `div` - const { width, height, layout } = this.props.image - const { - style: wStyle, - className: wClass, - ...wrapperProps - } = getWrapperProps(width, height, layout) - - let className = this.props.className - // preact class - if (this.props.class) { - className = this.props.class + // Trigger onStartload and onLoad events + props?.onStartLoad?.({ + wasCached: true, + }) + props?.onLoad?.({ + wasCached: true, + }) } - - const sizer = getSizer(layout, width, height) - - return ( - - ) - } + }, [props.image.images]) + + return createElement(props.as || `div`, { + ...wrapperProps, + style: { + ...wStyle, + ...props.style, + backgroundColor: props.backgroundColor, + }, + className: `${wClass}${className ? ` ${className}` : ``}`, + ref: root, + dangerouslySetInnerHTML: { + __html: sizer, + }, + suppressHydrationWarning: true, + }) } -export const GatsbyImage: FunctionComponent = +export const GatsbyImage: FunctionComponent = memo( function GatsbyImage(props) { if (!props.image) { if (process.env.NODE_ENV === `development`) { console.warn(`[gatsby-plugin-image] Missing image prop`) } + return null } @@ -315,19 +221,10 @@ export const GatsbyImage: FunctionComponent = `[gatsby-plugin-image] You're missing out on some cool performance features. Please add "gatsby-plugin-image" to your gatsby-config.js` ) } - const { className, class: classSafe, backgroundColor, image } = props - const { width, height, layout } = image - const propsKey = JSON.stringify([ - width, - height, - layout, - className, - classSafe, - backgroundColor, - ]) - return + + return createElement(GatsbyImageHydrator, props) } +) GatsbyImage.propTypes = propTypes - GatsbyImage.displayName = `GatsbyImage` diff --git a/packages/gatsby-plugin-image/src/components/gatsby-image.server.tsx b/packages/gatsby-plugin-image/src/components/gatsby-image.server.tsx index 40d6cab111edf..31848fab8ae4f 100644 --- a/packages/gatsby-plugin-image/src/components/gatsby-image.server.tsx +++ b/packages/gatsby-plugin-image/src/components/gatsby-image.server.tsx @@ -1,15 +1,16 @@ -import React, { - ElementType, - FunctionComponent, - CSSProperties, - WeakValidationMap, -} from "react" -import { GatsbyImageProps, IGatsbyImageData } from "./gatsby-image.browser" +import React from "react" import { getWrapperProps, getMainProps, getPlaceholderProps } from "./hooks" import { Placeholder } from "./placeholder" import { MainImage, MainImageProps } from "./main-image" import { LayoutWrapper } from "./layout-wrapper" import PropTypes from "prop-types" +import type { + ElementType, + FunctionComponent, + CSSProperties, + WeakValidationMap, +} from "react" +import type { GatsbyImageProps, IGatsbyImageData } from "./gatsby-image.browser" const removeNewLines = (str: string): string => str.replace(/\n/g, ``) @@ -40,9 +41,11 @@ export const GatsbyImage: FunctionComponent = console.warn(`[gatsby-plugin-image] Missing image prop`) return null } + if (preactClass) { className = preactClass } + imgStyle = { objectFit, objectPosition, @@ -115,16 +118,16 @@ export const GatsbyImage: FunctionComponent = )} + {...(props as Omit< + MainImageProps, + "images" | "fallback" | "onError" | "onLoad" + >)} // When eager is set we want to start the isLoading state on true (we want to load the img without react) {...getMainProps( loading === `eager`, false, cleanedImages, loading, - undefined, - undefined, - undefined, imgStyle )} /> @@ -144,8 +147,10 @@ export const altValidator: PropTypes.Validator = ( `The "alt" prop is required in ${componentName}. If the image is purely presentational then pass an empty string: e.g. alt="". Learn more: https://a11y-style-guide.com/style-guide/section-media.html` ) } + return PropTypes.string(props, propName, componentName, ...rest) } + export const propTypes = { image: PropTypes.object.isRequired, alt: altValidator, diff --git a/packages/gatsby-plugin-image/src/components/hooks.ts b/packages/gatsby-plugin-image/src/components/hooks.ts index 3e766d85a8c84..aae602757ca99 100644 --- a/packages/gatsby-plugin-image/src/components/hooks.ts +++ b/packages/gatsby-plugin-image/src/components/hooks.ts @@ -1,28 +1,16 @@ -/* eslint-disable no-unused-expressions */ -import { - useState, - CSSProperties, - useEffect, - HTMLAttributes, - ImgHTMLAttributes, - ReactEventHandler, - SetStateAction, - Dispatch, - RefObject, -} from "react" -import { Node } from "gatsby" -import { PlaceholderProps } from "./placeholder" -import { MainImageProps } from "./main-image" +/* global GATSBY___IMAGE */ +import { generateImageData, EVERY_BREAKPOINT } from "../image-utils" +import type { CSSProperties, HTMLAttributes, ImgHTMLAttributes } from "react" +import type { Node } from "gatsby" +import type { PlaceholderProps } from "./placeholder" +import type { MainImageProps } from "./main-image" import type { IGatsbyImageData } from "./gatsby-image.browser" -import { +import type { IGatsbyImageHelperArgs, - generateImageData, Layout, - EVERY_BREAKPOINT, IImage, ImageFormat, } from "../image-utils" -const imageCache = new Set() // Native lazy-loading support: https://addyosmani.com/blog/lazy-loading/ export const hasNativeLazyLoadSupport = (): boolean => @@ -33,15 +21,6 @@ export function gatsbyImageIsInstalled(): boolean { return typeof GATSBY___IMAGE !== `undefined` && GATSBY___IMAGE } -export function storeImageloaded(cacheKey?: string): void { - if (cacheKey) { - imageCache.add(cacheKey) - } -} - -export function hasImageLoaded(cacheKey: string): boolean { - return imageCache.has(cacheKey) -} export type IGatsbyImageDataParent = T & { gatsbyImageData: IGatsbyImageData } @@ -113,18 +92,6 @@ export function getWrapperProps( } } -export async function applyPolyfill( - ref: RefObject -): Promise { - if (!(`objectFitPolyfill` in window)) { - await import( - // @ts-ignore typescript can't find the module for some reason ¯\_(ツ)_/¯ - /* webpackChunkName: "gatsby-plugin-image-objectfit-polyfill" */ `objectFitPolyfill` - ) - } - ;(window as any).objectFitPolyfill(ref.current) -} - export interface IUrlBuilderArgs { width: number height: number @@ -234,43 +201,14 @@ export function getMainProps( isLoaded: boolean, images: IGatsbyImageData["images"], loading?: "eager" | "lazy", - toggleLoaded?: (loaded: boolean) => void, - cacheKey?: string, - ref?: RefObject, style: CSSProperties = {} ): Partial { - const onLoad: ReactEventHandler = function (e) { - if (isLoaded) { - return - } - - storeImageloaded(cacheKey) - - const target = e.currentTarget - const img = new Image() - img.src = target.currentSrc - - if (img.decode) { - // Decode the image through javascript to support our transition - img - .decode() - .catch(() => { - // ignore error, we just go forward - }) - .then(() => { - toggleLoaded(true) - }) - } else { - toggleLoaded(true) - } - } - - // Polyfill "object-fit" if unsupported (mostly IE) - if (ref?.current && !(`objectFit` in document.documentElement.style)) { - ref.current.dataset.objectFit = style.objectFit ?? `cover` - ref.current.dataset.objectPosition = `${style.objectPosition ?? `50% 50%`}` - applyPolyfill(ref) - } + // // Polyfill "object-fit" if unsupported (mostly IE) + // if (ref?.current && !(`objectFit` in document.documentElement.style)) { + // ref.current.dataset.objectFit = style.objectFit ?? `cover` + // ref.current.dataset.objectPosition = `${style.objectPosition ?? `50% 50%`}` + // applyPolyfill(ref) + // } // fallback when it's not configured in gatsby-config. if (!gatsbyImageIsInstalled()) { @@ -296,8 +234,6 @@ export function getMainProps( ...style, opacity: isLoaded ? 1 : 0, }, - onLoad, - ref, } return result @@ -375,58 +311,6 @@ export function getPlaceholderProps( return result } -export function useImageLoaded( - cacheKey: string, - loading: "lazy" | "eager", - ref: any -): { - isLoaded: boolean - isLoading: boolean - toggleLoaded: Dispatch> -} { - const [isLoaded, toggleLoaded] = useState(false) - const [isLoading, toggleIsLoading] = useState(loading === `eager`) - - const rAF = - typeof window !== `undefined` && `requestAnimationFrame` in window - ? requestAnimationFrame - : function (cb: TimerHandler): number { - return setTimeout(cb, 16) - } - const cRAF = - typeof window !== `undefined` && `cancelAnimationFrame` in window - ? cancelAnimationFrame - : clearTimeout - - useEffect(() => { - let interval: number - // @see https://stackoverflow.com/questions/44074747/componentdidmount-called-before-ref-callback/50019873#50019873 - function toggleIfRefExists(): void { - if (ref.current) { - if (loading === `eager` && ref.current.complete) { - storeImageloaded(cacheKey) - toggleLoaded(true) - } else { - toggleIsLoading(true) - } - } else { - interval = rAF(toggleIfRefExists) - } - } - toggleIfRefExists() - - return (): void => { - cRAF(interval) - } - }, []) - - return { - isLoading, - isLoaded, - toggleLoaded, - } -} - export interface IArtDirectedImage { media: string image: IGatsbyImageData diff --git a/packages/gatsby-plugin-image/src/components/intersection-observer.ts b/packages/gatsby-plugin-image/src/components/intersection-observer.ts index 9a632db59a72a..dc52ba790a8a2 100644 --- a/packages/gatsby-plugin-image/src/components/intersection-observer.ts +++ b/packages/gatsby-plugin-image/src/components/intersection-observer.ts @@ -1,6 +1,3 @@ -/* eslint-disable no-unused-expressions */ -import { RefObject } from "react" - let intersectionObserver: IntersectionObserver export type Unobserver = () => void @@ -20,7 +17,7 @@ const SLOW_CONNECTION_THRESHOLD = `2500px` export function createIntersectionObserver( callback: () => void -): (element: RefObject) => Unobserver { +): (element: HTMLElement) => Unobserver { const connectionType = connection?.effectiveType // if we don't support intersectionObserver we don't lazy load (Sorry IE 11). @@ -52,19 +49,15 @@ export function createIntersectionObserver( ) } - return function observe( - element: RefObject - ): Unobserver { - if (element.current) { - // Store a reference to the callback mapped to the element being watched - ioEntryMap.set(element.current, callback) - intersectionObserver.observe(element.current) - } + return function observe(element: HTMLElement): Unobserver { + // Store a reference to the callback mapped to the element being watched + ioEntryMap.set(element, callback) + intersectionObserver.observe(element) return function unobserve(): void { - if (intersectionObserver && element.current) { - ioEntryMap.delete(element.current) - intersectionObserver.unobserve(element.current) + if (intersectionObserver && element) { + ioEntryMap.delete(element) + intersectionObserver.unobserve(element) } } } diff --git a/packages/gatsby-plugin-image/src/components/later-hydrator.tsx b/packages/gatsby-plugin-image/src/components/later-hydrator.tsx deleted file mode 100644 index 85b70825ad4ea..0000000000000 --- a/packages/gatsby-plugin-image/src/components/later-hydrator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from "react" -export function LaterHydrator({ - children, -}: React.PropsWithChildren>): React.ReactNode { - React.useEffect(() => { - // eslint-disable-next-line no-unused-expressions - import(`./lazy-hydrate`) - }, []) - - return children -} diff --git a/packages/gatsby-plugin-image/src/components/layout-wrapper.tsx b/packages/gatsby-plugin-image/src/components/layout-wrapper.tsx index 0634d223a6dfe..6c0d4973ec9c3 100644 --- a/packages/gatsby-plugin-image/src/components/layout-wrapper.tsx +++ b/packages/gatsby-plugin-image/src/components/layout-wrapper.tsx @@ -1,6 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// - import React, { Fragment, FunctionComponent } from "react" import terserMacro from "../../macros/terser.macro" import { Layout } from "../image-utils" @@ -56,9 +53,11 @@ export function getSizer( (height / width) * 100 }%;">` } + if (layout === `constrained`) { sizer = `
` } + return sizer } @@ -72,6 +71,7 @@ const Sizer: FunctionComponent = function Sizer({
) } + if (layout === `constrained`) { return (
@@ -100,10 +100,7 @@ export const LayoutWrapper: FunctionComponent = {children} - { - // eslint-disable-next-line no-undef - SERVER && - } + {SERVER ? : null} ) } diff --git a/packages/gatsby-plugin-image/src/components/lazy-hydrate.tsx b/packages/gatsby-plugin-image/src/components/lazy-hydrate.tsx index 41caf32644481..bce1b956d9930 100644 --- a/packages/gatsby-plugin-image/src/components/lazy-hydrate.tsx +++ b/packages/gatsby-plugin-image/src/components/lazy-hydrate.tsx @@ -1,72 +1,178 @@ -/* global HAS_REACT_18 */ -import React, { MutableRefObject } from "react" -import { GatsbyImageProps } from "./gatsby-image.browser" +import React from "react" +import { renderToStaticMarkup } from "react-dom/server" import { LayoutWrapper } from "./layout-wrapper" import { Placeholder } from "./placeholder" -import { MainImageProps, MainImage } from "./main-image" -import { getMainProps, getPlaceholderProps } from "./hooks" -import { ReactElement } from "react" -import type { Root } from "react-dom/client" +import { MainImage } from "./main-image" +import { + hasNativeLazyLoadSupport, + getMainProps, + getPlaceholderProps, +} from "./hooks" +import { createIntersectionObserver } from "./intersection-observer" +import type { MainImageProps } from "./main-image" +import type { GatsbyImageProps } from "./gatsby-image.browser" type LazyHydrateProps = Omit & { isLoading: boolean - isLoaded: boolean // alwaystype SetStateAction = S | ((prevState: S) => S); - toggleIsLoaded: (toggle: boolean) => void - ref: MutableRefObject + isLoaded: boolean } -let reactRender -let reactHydrate -if (HAS_REACT_18) { - const reactDomClient = require(`react-dom/client`) - reactRender = ( - Component: React.ReactChild | Iterable, - el: ReactDOM.Container, - root: Root - ): Root => { - if (!root) { - root = reactDomClient.createRoot(el) +async function applyPolyfill(element: HTMLImageElement): Promise { + if (!(`objectFitPolyfill` in window)) { + await import( + // @ts-ignore typescript can't find the module for some reason ¯\_(ツ)_/¯ + /* webpackChunkName: "gatsby-plugin-image-objectfit-polyfill" */ `objectFitPolyfill` + ) + } + ;(window as any).objectFitPolyfill(element) +} + +function toggleLoaded( + mainImage: HTMLElement, + placeholderImage: HTMLElement +): void { + mainImage.style.opacity = `1` + + if (placeholderImage) { + placeholderImage.style.opacity = `0` + } +} + +function startLoading( + element: HTMLElement, + cacheKey: string, + imageCache: Set, + onStartLoad: GatsbyImageProps["onStartLoad"], + onLoad: GatsbyImageProps["onLoad"], + onError: GatsbyImageProps["onError"] +): () => void { + const mainImage = element.querySelector(`[data-main-image]`) + const placeholderImage = element.querySelector( + `[data-placeholder-image]` + ) + const isCached = imageCache.has(cacheKey) + + function onImageLoaded(e): void { + // eslint-disable-next-line @babel/no-invalid-this + this.removeEventListener(`load`, onImageLoaded) + + const target = e.currentTarget + const img = new Image() + img.src = target.currentSrc + + if (img.decode) { + // Decode the image through javascript to support our transition + img + .decode() + .then(() => { + // eslint-disable-next-line @babel/no-invalid-this + toggleLoaded(this, placeholderImage) + onLoad?.({ + wasCached: isCached, + }) + }) + .catch(e => { + // eslint-disable-next-line @babel/no-invalid-this + toggleLoaded(this, placeholderImage) + onError?.(e) + }) + } else { + // eslint-disable-next-line @babel/no-invalid-this + toggleLoaded(this, placeholderImage) + onLoad?.({ + wasCached: isCached, + }) + } + } + + mainImage.addEventListener(`load`, onImageLoaded) + + onStartLoad?.({ + wasCached: isCached, + }) + Array.from(mainImage.parentElement.children).forEach(child => { + const src = child.getAttribute(`data-src`) + const srcSet = child.getAttribute(`data-srcset`) + if (src) { + child.removeAttribute(`data-src`) + child.setAttribute(`src`, src) + } + if (srcSet) { + child.removeAttribute(`data-srcset`) + child.setAttribute(`srcset`, srcSet) } + }) - root.render(Component) + imageCache.add(cacheKey) - return root + return (): void => { + if (mainImage) { + mainImage.removeEventListener(`load`, onImageLoaded) + } } - reactHydrate = ( - Component: React.ReactChild | Iterable, - el: ReactDOM.Container - ): Root => reactDomClient.hydrateRoot(el, Component) -} else { - const reactDomClient = require(`react-dom`) - reactRender = ( - Component: React.ReactChild | Iterable, - el: ReactDOM.Container - ): void => { - reactDomClient.render(Component, el) +} + +export function swapPlaceholderImage( + element: HTMLElement, + cacheKey: string, + imageCache: Set, + style: React.CSSProperties, + onStartLoad: GatsbyImageProps["onStartLoad"], + onLoad: GatsbyImageProps["onLoad"], + onError: GatsbyImageProps["onError"] +): () => void { + if (!hasNativeLazyLoadSupport()) { + let cleanup + const io = createIntersectionObserver(() => { + cleanup = startLoading( + element, + cacheKey, + imageCache, + onStartLoad, + onLoad, + onError + ) + }) + const unobserve = io(element) + + // Polyfill "object-fit" if unsupported (mostly IE) + if (!(`objectFit` in document.documentElement.style)) { + element.dataset.objectFit = style.objectFit ?? `cover` + element.dataset.objectPosition = `${style.objectPosition ?? `50% 50%`}` + applyPolyfill(element as HTMLImageElement) + } + + return (): void => { + if (cleanup) { + cleanup() + } + + unobserve() + } } - reactHydrate = reactDomClient.hydrate + + return startLoading( + element, + cacheKey, + imageCache, + onStartLoad, + onLoad, + onError + ) } -export function lazyHydrate( - { - image, - loading, - isLoading, - isLoaded, - toggleIsLoaded, - ref, - imgClassName, - imgStyle = {}, - objectPosition, - backgroundColor, - objectFit = `cover`, - ...props - }: LazyHydrateProps, - root: MutableRefObject, - hydrated: MutableRefObject, - forceHydrate: MutableRefObject, - reactRootRef: MutableRefObject -): (() => void) | null { +export function renderImageToString({ + image, + loading = `lazy`, + isLoading, + isLoaded, + imgClassName, + imgStyle = {}, + objectPosition, + backgroundColor, + objectFit = `cover`, + ...props +}: LazyHydrateProps): string { const { width, height, @@ -76,8 +182,6 @@ export function lazyHydrate( backgroundColor: wrapperBackgroundColor, } = image - const cacheKey = JSON.stringify(images) - imgStyle = { objectFit, objectPosition, @@ -85,7 +189,7 @@ export function lazyHydrate( ...imgStyle, } - const component = ( + return renderToStaticMarkup( )} + {...(props as Omit< + MainImageProps, + "images" | "fallback" | "onLoad" | "onError" + >)} width={width} height={height} className={imgClassName} - {...getMainProps( - isLoading, - isLoaded, - images, - loading, - toggleIsLoaded, - cacheKey, - ref, - imgStyle - )} + {...getMainProps(isLoading, isLoaded, images, loading, imgStyle)} /> ) - - if (root.current) { - // Force render to mitigate "Expected server HTML to contain a matching" in develop - if (hydrated.current || forceHydrate.current || HAS_REACT_18) { - reactRootRef.current = reactRender( - component, - root.current, - reactRootRef.current - ) - } else { - reactHydrate(component, root.current) - } - hydrated.current = true - } - - return (): void => { - if (root.current) { - reactRender( - null as unknown as ReactElement, - root.current, - reactRootRef.current - ) - } - } } diff --git a/packages/gatsby-plugin-image/src/components/main-image.tsx b/packages/gatsby-plugin-image/src/components/main-image.tsx index dc40776873e3b..398e2fe36702f 100644 --- a/packages/gatsby-plugin-image/src/components/main-image.tsx +++ b/packages/gatsby-plugin-image/src/components/main-image.tsx @@ -1,20 +1,18 @@ -import React, { forwardRef } from "react" +import React from "react" import { Picture, PictureProps } from "./picture" export type MainImageProps = PictureProps -export const MainImage = forwardRef( - function MainImage(props, ref) { - return ( - <> - - - - ) - } -) +export const MainImage: React.FC = function MainImage(props) { + return ( + <> + + + + ) +} MainImage.displayName = `MainImage` MainImage.propTypes = Picture.propTypes diff --git a/packages/gatsby-plugin-image/src/components/picture.tsx b/packages/gatsby-plugin-image/src/components/picture.tsx index 3118d3135c20c..874ff7cdd2bbc 100644 --- a/packages/gatsby-plugin-image/src/components/picture.tsx +++ b/packages/gatsby-plugin-image/src/components/picture.tsx @@ -1,10 +1,4 @@ -/* eslint-disable filenames/match-regex */ -import React, { - FunctionComponent, - ImgHTMLAttributes, - forwardRef, - LegacyRef, -} from "react" +import React, { FunctionComponent, ImgHTMLAttributes } from "react" import * as PropTypes from "prop-types" export interface IResponsiveImageProps { @@ -30,7 +24,6 @@ type ImageProps = ImgHTMLAttributes & { src: string alt: string shouldLoad: boolean - innerRef: LegacyRef } export type PictureProps = ImgHTMLAttributes & { @@ -46,7 +39,6 @@ const Image: FunctionComponent = function Image({ loading, alt = ``, shouldLoad, - innerRef, ...props }) { return ( @@ -59,48 +51,41 @@ const Image: FunctionComponent = function Image({ srcSet={shouldLoad ? srcSet : undefined} data-srcset={!shouldLoad ? srcSet : undefined} alt={alt} - ref={innerRef} /> ) } -export const Picture = forwardRef( - function Picture( - { fallback, sources = [], shouldLoad = true, ...props }, - ref - ) { - const sizes = props.sizes || fallback?.sizes - const fallbackImage = ( - - ) - - if (!sources.length) { - return fallbackImage - } +export const Picture: React.FC = function Picture({ + fallback, + sources = [], + shouldLoad = true, + ...props +}) { + const sizes = props.sizes || fallback?.sizes + const fallbackImage = ( + + ) - return ( - - {sources.map(({ media, srcSet, type }) => ( - - ))} - {fallbackImage} - - ) + if (!sources.length) { + return fallbackImage } -) + + return ( + + {sources.map(({ media, srcSet, type }) => ( + + ))} + {fallbackImage} + + ) +} Image.propTypes = { src: PropTypes.string.isRequired, diff --git a/packages/gatsby-plugin-image/src/components/placeholder.tsx b/packages/gatsby-plugin-image/src/components/placeholder.tsx index ef99e5ba78368..cfc754f823524 100644 --- a/packages/gatsby-plugin-image/src/components/placeholder.tsx +++ b/packages/gatsby-plugin-image/src/components/placeholder.tsx @@ -33,6 +33,7 @@ Placeholder.propTypes = { if (!props[propName]) { return null } + return new Error( `Invalid prop \`${propName}\` supplied to \`${componentName}\`. Validation failed.` ) diff --git a/packages/gatsby-plugin-image/src/gatsby-node.ts b/packages/gatsby-plugin-image/src/gatsby-node.ts index 9de986c24fb45..f76e3c9c1360d 100644 --- a/packages/gatsby-plugin-image/src/gatsby-node.ts +++ b/packages/gatsby-plugin-image/src/gatsby-node.ts @@ -1,11 +1,10 @@ -import { GatsbyNode } from "gatsby" +import type { GatsbyNode } from "gatsby" import { getCacheDir } from "./node-apis/node-utils" import { ImageFormatType, ImageLayoutType, ImagePlaceholderType, } from "./resolver-utils" -import { major } from "semver" export * from "./node-apis/preprocess-source" @@ -52,9 +51,6 @@ export const onCreateWebpackConfig: GatsbyNode["onCreateWebpackConfig"] = ({ plugins.define({ // eslint-disable-next-line @typescript-eslint/naming-convention GATSBY___IMAGE: true, - HAS_REACT_18: JSON.stringify( - major(require(`react-dom/package.json`).version) >= 18 - ), }), ], }) diff --git a/packages/gatsby-plugin-image/src/global.d.ts b/packages/gatsby-plugin-image/src/global.d.ts deleted file mode 100644 index a76544f65b970..0000000000000 --- a/packages/gatsby-plugin-image/src/global.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export {} - -declare global { - declare var SERVER: boolean | undefined - declare var GATSBY___IMAGE: boolean | undefined - declare var HAS_REACT_18: boolean | undefined -} diff --git a/packages/gatsby-plugin-image/src/global.ts b/packages/gatsby-plugin-image/src/global.ts new file mode 100644 index 0000000000000..eeed9e581b962 --- /dev/null +++ b/packages/gatsby-plugin-image/src/global.ts @@ -0,0 +1,12 @@ +export {} + +declare global { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Window { + __GATSBY_IMAGE_FIRST_RENDER: boolean + } + + export const SERVER: boolean | undefined + // eslint-disable-next-line @typescript-eslint/naming-convention + export const GATSBY___IMAGE: boolean | undefined +} diff --git a/packages/gatsby-plugin-image/src/image-utils.ts b/packages/gatsby-plugin-image/src/image-utils.ts index 7f058b17198e7..221b3d88e43eb 100644 --- a/packages/gatsby-plugin-image/src/image-utils.ts +++ b/packages/gatsby-plugin-image/src/image-utils.ts @@ -1,7 +1,5 @@ -/* eslint-disable no-unused-expressions */ -import { stripIndent } from "common-tags" import camelCase from "camelcase" -import { IGatsbyImageData } from "." +import type { IGatsbyImageData } from "./index" const DEFAULT_PIXEL_DENSITIES = [0.25, 0.5, 1, 2] export const DEFAULT_BREAKPOINTS = [750, 1080, 1366, 1920] @@ -451,8 +449,8 @@ export function fixedImageSizes({ // print out this message with the necessary information before we overwrite it for sizing if (isTopSizeOverriden) { const fixedDimension = imgDimensions.width < width ? `width` : `height` - reporter.warn(stripIndent` - The requested ${fixedDimension} "${ + reporter.warn(` +The requested ${fixedDimension} "${ fixedDimension === `width` ? width : height }px" for the image ${filename} was larger than the actual image ${fixedDimension} of ${ imgDimensions[fixedDimension] diff --git a/packages/gatsby-plugin-image/src/index.browser.ts b/packages/gatsby-plugin-image/src/index.browser.ts index f804c2a7e74db..daff94cc0524e 100644 --- a/packages/gatsby-plugin-image/src/index.browser.ts +++ b/packages/gatsby-plugin-image/src/index.browser.ts @@ -1,3 +1,4 @@ +import "./global" export { GatsbyImage, GatsbyImageProps, @@ -6,7 +7,6 @@ export { export { Placeholder } from "./components/placeholder" export { MainImage } from "./components/main-image" export { StaticImage } from "./components/static-image" -export { LaterHydrator } from "./components/later-hydrator" export { getImage, getSrc, diff --git a/packages/gatsby-plugin-image/src/index.ts b/packages/gatsby-plugin-image/src/index.ts index 5961811df1506..de68d6b5ad7d3 100644 --- a/packages/gatsby-plugin-image/src/index.ts +++ b/packages/gatsby-plugin-image/src/index.ts @@ -1,3 +1,4 @@ +import "./global" export { GatsbyImage } from "./components/gatsby-image.server" export { GatsbyImageProps, diff --git a/packages/gatsby-plugin-image/src/resolver-utils.ts b/packages/gatsby-plugin-image/src/resolver-utils.ts index b21a3ab991ff2..76686fd7c8523 100644 --- a/packages/gatsby-plugin-image/src/resolver-utils.ts +++ b/packages/gatsby-plugin-image/src/resolver-utils.ts @@ -1,11 +1,11 @@ -import { GraphQLFieldResolver } from "gatsby/graphql" -import { +import { stripIndent } from "common-tags" +import type { GraphQLFieldResolver } from "gatsby/graphql" +import type { EnumTypeComposerAsObjectDefinition, ObjectTypeComposerFieldConfigAsObjectDefinition, ObjectTypeComposerArgumentConfigMapDefinition, } from "graphql-compose" -import { stripIndent } from "common-tags" -import { ISharpGatsbyImageArgs, IImageSizeArgs } from "./image-utils" +import type { ISharpGatsbyImageArgs, IImageSizeArgs } from "./image-utils" export const ImageFormatType: EnumTypeComposerAsObjectDefinition = { name: `GatsbyImageFormat`, diff --git a/packages/gatsby-plugin-image/tsconfig.json b/packages/gatsby-plugin-image/tsconfig.json index 9f13f0cb51a9e..adcd64efa9520 100644 --- a/packages/gatsby-plugin-image/tsconfig.json +++ b/packages/gatsby-plugin-image/tsconfig.json @@ -14,5 +14,5 @@ "moduleResolution": "node" // "jsxFactory": "createElement" }, - "files": ["./src/global.d.ts", "./src/index.ts", "./src/index.browser.ts"] + "files": ["./src/global.ts", "./src/index.ts", "./src/index.browser.ts"] } From 4d0524a032d30520abc7a8ab9449fdc48f6152a0 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Sun, 27 Mar 2022 11:59:00 +0200 Subject: [PATCH 02/17] update package.json --- packages/gatsby-plugin-image/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby-plugin-image/package.json b/packages/gatsby-plugin-image/package.json index 6f0e076df8364..4782b00502a60 100644 --- a/packages/gatsby-plugin-image/package.json +++ b/packages/gatsby-plugin-image/package.json @@ -7,7 +7,7 @@ "build:gatsby-ssr": "microbundle -i src/gatsby-ssr.tsx -f cjs -o ./[name].js --no-pkg-main --jsx React.createElement --no-compress --external=common-tags,react --no-sourcemap", "build:server": "microbundle -f cjs,es --jsx React.createElement --define SERVER=true", "build:browser": "microbundle -i src/index.browser.ts -f cjs,modern --jsx React.createElement -o dist/gatsby-image.browser --define SERVER=false", - "prepare": "build", + "prepare": "yarn build", "watch": "npm-run-all -s clean -p watch:*", "watch:gatsby-node": "yarn build:gatsby-node --watch", "watch:gatsby-ssr": "yarn build:gatsby-ssr watch", From 2042f537f10429cd3c0d32ac3d298bbd65cd48ed Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Wed, 30 Mar 2022 23:30:39 +0200 Subject: [PATCH 03/17] fix tests --- .../gatsby-plugin-image/gatsby-browser.js | 6 -- packages/gatsby-plugin-image/package.json | 4 +- .../__tests__/gatsby-image.browser.tsx | 11 +-- .../src/components/gatsby-image.browser.tsx | 77 +++++++++++-------- 4 files changed, 51 insertions(+), 47 deletions(-) delete mode 100644 packages/gatsby-plugin-image/gatsby-browser.js diff --git a/packages/gatsby-plugin-image/gatsby-browser.js b/packages/gatsby-plugin-image/gatsby-browser.js deleted file mode 100644 index 7ca0ef15141ce..0000000000000 --- a/packages/gatsby-plugin-image/gatsby-browser.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from "react" -import { LaterHydrator } from "." - -export function wrapRootElement({ element }) { - return {element} -} diff --git a/packages/gatsby-plugin-image/package.json b/packages/gatsby-plugin-image/package.json index 4782b00502a60..86f9cfaefeee8 100644 --- a/packages/gatsby-plugin-image/package.json +++ b/packages/gatsby-plugin-image/package.json @@ -27,7 +27,7 @@ "esmodule": "dist/gatsby-image.modern.js", "browser": { "./dist/gatsby-image.js": "./dist/gatsby-image.browser.js", - "./dist/gatsby-image.modern.js": "./dist/gatsby-image.browser.modern.js" + "./dist/gatsby-image.module.js": "./dist/gatsby-image.browser.module.js" }, "files": [ "dist/*", @@ -93,4 +93,4 @@ }, "author": "Matt Kane ", "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx b/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx index 25fbcde9c147d..154d8617a5b75 100644 --- a/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx +++ b/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx @@ -1,8 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /** * @jest-environment jsdom */ - import React from "react" import { render, waitFor } from "@testing-library/react" import * as hooks from "../hooks" @@ -145,10 +143,13 @@ describe(`GatsbyImage browser`, () => { { container: beforeHydrationContent, hydrate: true } ) - const placeholder = await waitFor(() => - container.querySelector(`[data-placeholder-image=""]`) + const placeholder = await waitFor( + () => + container.querySelector(`[data-placeholder-image=""]`) as HTMLElement ) - const mainImage = container.querySelector(`[data-main-image=""]`) + const mainImage = container.querySelector( + `[data-main-image=""]` + ) as HTMLElement expect(placeholder).toBeDefined() expect(mainImage).toBeDefined() diff --git a/packages/gatsby-plugin-image/src/components/gatsby-image.browser.tsx b/packages/gatsby-plugin-image/src/components/gatsby-image.browser.tsx index de0f809a680b6..a008c015e2e45 100644 --- a/packages/gatsby-plugin-image/src/components/gatsby-image.browser.tsx +++ b/packages/gatsby-plugin-image/src/components/gatsby-image.browser.tsx @@ -6,7 +6,11 @@ import { useLayoutEffect, useRef, } from "react" -import { getWrapperProps, gatsbyImageIsInstalled } from "./hooks" +import { + getWrapperProps, + gatsbyImageIsInstalled, + hasNativeLazyLoadSupport, +} from "./hooks" import { getSizer } from "./layout-wrapper" import { propTypes } from "./gatsby-image.server" import type { @@ -56,25 +60,30 @@ export interface IGatsbyImageData { placeholder?: Pick } -const GatsbyImageHydrator: FC = function GatsbyImageHydrator( - props -) { - const { width, height, layout } = props.image +const GatsbyImageHydrator: FC = function GatsbyImageHydrator({ + as = `div`, + image, + style, + backgroundColor, + className, + class: preactClass, + onStartLoad, + onLoad, + onError, + ...props +}) { + const { width, height, layout } = image const { style: wStyle, className: wClass, ...wrapperProps } = getWrapperProps(width, height, layout) const root = useRef() - const cacheKey = useMemo( - () => JSON.stringify(props.image.images), - [props.image.images] - ) + const cacheKey = useMemo(() => JSON.stringify(image.images), [image.images]) - let className = props.className // Preact uses class instead of className so we need to check for both - if (props.class) { - className = props.class + if (preactClass) { + className = preactClass } const sizer = getSizer(layout, width, height) @@ -98,23 +107,23 @@ const GatsbyImageHydrator: FC = function GatsbyImageHydrator( const ssrImage = root.current.querySelector( `[data-gatsby-image-ssr]` ) as HTMLImageElement - if (ssrImage) { + if (ssrImage && hasNativeLazyLoadSupport()) { if (ssrImage.complete) { // Trigger onStartload and onLoad events - props?.onStartLoad?.({ + onStartLoad?.({ wasCached: true, }) - props?.onLoad?.({ + onLoad?.({ wasCached: true, }) } else { - document.addEventListener(`load`, function onLoad() { - document.removeEventListener(`load`, onLoad) + document.addEventListener(`load`, function onLoadListener() { + document.removeEventListener(`load`, onLoadListener) - props?.onStartLoad?.({ + onStartLoad?.({ wasCached: true, }) - props?.onLoad?.({ + onLoad?.({ wasCached: true, }) }) @@ -134,9 +143,9 @@ const GatsbyImageHydrator: FC = function GatsbyImageHydrator( renderImageToStringPromise.then( ({ renderImageToString, swapPlaceholderImage }) => { root.current.innerHTML = renderImageToString({ - image: props.image.images, isLoading: true, isLoaded: imageCache.has(cacheKey), + image, ...props, }) @@ -147,10 +156,10 @@ const GatsbyImageHydrator: FC = function GatsbyImageHydrator( root.current, cacheKey, imageCache, - props.style, - props.onStartLoad, - props.onLoad, - props.onError + style, + onStartLoad, + onLoad, + onError ) } }) @@ -167,35 +176,35 @@ const GatsbyImageHydrator: FC = function GatsbyImageHydrator( cleanupCallback() } } - }, [props.image.images]) + }, [image]) - // We need to run this effect before browser has paint to make sure our html is set so no flickering happens - // + // useLayoutEffect is ran before React commits to the DOM. This allows us to make sure our HTML is using our cached image version useLayoutEffect(() => { if (imageCache.has(cacheKey) && renderImage) { root.current.innerHTML = renderImage({ - image: props.image.images, isLoading: imageCache.has(cacheKey), isLoaded: imageCache.has(cacheKey), + image, ...props, }) // Trigger onStartload and onLoad events - props?.onStartLoad?.({ + onStartLoad?.({ wasCached: true, }) - props?.onLoad?.({ + onLoad?.({ wasCached: true, }) } - }, [props.image.images]) + }, [image]) - return createElement(props.as || `div`, { + // By keeping all props equal React will keep the component in the DOM + return createElement(as, { ...wrapperProps, style: { ...wStyle, - ...props.style, - backgroundColor: props.backgroundColor, + ...style, + backgroundColor, }, className: `${wClass}${className ? ` ${className}` : ``}`, ref: root, From 909a17b72e496d8f36636b687445bca3de9bacc3 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Wed, 30 Mar 2022 23:51:57 +0200 Subject: [PATCH 04/17] fix mapping --- packages/gatsby-plugin-image/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby-plugin-image/package.json b/packages/gatsby-plugin-image/package.json index 86f9cfaefeee8..bfa6fb4e9bd9d 100644 --- a/packages/gatsby-plugin-image/package.json +++ b/packages/gatsby-plugin-image/package.json @@ -27,7 +27,7 @@ "esmodule": "dist/gatsby-image.modern.js", "browser": { "./dist/gatsby-image.js": "./dist/gatsby-image.browser.js", - "./dist/gatsby-image.module.js": "./dist/gatsby-image.browser.module.js" + "./dist/gatsby-image.module.js": "./dist/gatsby-image.browser.modern.js" }, "files": [ "dist/*", From c71c19a06b2af166bb3d406867d9ed0acce07ca9 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Wed, 6 Apr 2022 22:01:52 +0200 Subject: [PATCH 05/17] fix layout fixed --- .../gatsby-plugin-image/src/components/layout-wrapper.tsx | 2 +- scripts/cypress-run-with-conditional-record-flag.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/gatsby-plugin-image/src/components/layout-wrapper.tsx b/packages/gatsby-plugin-image/src/components/layout-wrapper.tsx index 6c0d4973ec9c3..b0155f95f6199 100644 --- a/packages/gatsby-plugin-image/src/components/layout-wrapper.tsx +++ b/packages/gatsby-plugin-image/src/components/layout-wrapper.tsx @@ -47,7 +47,7 @@ export function getSizer( width: number, height: number ): string { - let sizer: string | null = null + let sizer = `` if (layout === `fullWidth`) { sizer = `