From 2361f996a158a68e48fb7a12c1b12f8857826d9a Mon Sep 17 00:00:00 2001 From: rauno Date: Fri, 4 Apr 2025 14:08:30 +0300 Subject: [PATCH 1/2] [dev-overlay] Fix error dialog resizing logic --- .../ui/components/dialog/dialog.tsx | 18 +---- .../ui/components/dialog/styles.ts | 8 +- .../error-overlay-layout.tsx | 73 +++++++++++++------ .../errors/error-overlay/error-overlay.tsx | 4 + .../ui/components/resizer/index.tsx | 65 +++++++++++++++++ .../react-dev-overlay/ui/container/errors.tsx | 14 +--- .../ui/dev-overlay.stories.tsx | 3 +- .../react-dev-overlay/ui/dev-overlay.tsx | 1 + .../ui/hooks/use-measure-height.ts | 35 --------- 9 files changed, 127 insertions(+), 94 deletions(-) create mode 100644 packages/next/src/client/components/react-dev-overlay/ui/components/resizer/index.tsx delete mode 100644 packages/next/src/client/components/react-dev-overlay/ui/hooks/use-measure-height.ts diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/dialog/dialog.tsx b/packages/next/src/client/components/react-dev-overlay/ui/components/dialog/dialog.tsx index fa329e7965003..43f4dd444aca9 100644 --- a/packages/next/src/client/components/react-dev-overlay/ui/components/dialog/dialog.tsx +++ b/packages/next/src/client/components/react-dev-overlay/ui/components/dialog/dialog.tsx @@ -1,6 +1,5 @@ import * as React from 'react' import { useOnClickOutside } from '../../hooks/use-on-click-outside' -import { useMeasureHeight } from '../../hooks/use-measure-height' export type DialogProps = { children?: React.ReactNode @@ -37,9 +36,6 @@ const Dialog: React.FC = function Dialog({ : undefined ) - const ref = React.useRef(null) - const [height, pristine] = useMeasureHeight(ref) - useOnClickOutside( dialogRef.current, CSS_SELECTORS_TO_EXCLUDE_ON_CLICK_OUTSIDE, @@ -102,19 +98,7 @@ const Dialog: React.FC = function Dialog({ }} {...props} > -
-
{children}
-
+ {children} ) } diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/dialog/styles.ts b/packages/next/src/client/components/react-dev-overlay/ui/components/dialog/styles.ts index c8f1304172350..1838017af812c 100644 --- a/packages/next/src/client/components/react-dev-overlay/ui/components/dialog/styles.ts +++ b/packages/next/src/client/components/react-dev-overlay/ui/components/dialog/styles.ts @@ -3,7 +3,7 @@ const styles = ` --next-dialog-radius: var(--rounded-xl); --next-dialog-max-width: 960px; --next-dialog-row-padding: 16px; - --next-dialog-padding-x: 12px; + --next-dialog-padding: 12px; --next-dialog-notch-height: 42px; --next-dialog-border-width: 1px; @@ -14,7 +14,7 @@ const styles = ` max-width: var(--next-dialog-max-width); margin-right: auto; margin-left: auto; - scale: 0.98; + scale: 0.97; opacity: 0; transition-property: scale, opacity; transition-duration: var(--transition-duration); @@ -28,7 +28,7 @@ const styles = ` [data-nextjs-scroll-fader][data-side="top"] { left: 1px; top: calc(var(--next-dialog-notch-height) + var(--next-dialog-border-width)); - width: calc(100% - var(--next-dialog-padding-x)); + width: calc(100% - var(--next-dialog-padding)); opacity: 0; } } @@ -82,7 +82,7 @@ const styles = ` display: flex; flex-direction: column; position: relative; - padding: 16px var(--next-dialog-padding-x); + padding: var(--next-dialog-padding); } [data-nextjs-dialog-content] > [data-nextjs-dialog-header] { diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-layout/error-overlay-layout.tsx b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-layout/error-overlay-layout.tsx index 8032fca364faa..2d752d6b87b94 100644 --- a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-layout/error-overlay-layout.tsx +++ b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-layout/error-overlay-layout.tsx @@ -36,6 +36,7 @@ import type { ReadyRuntimeError } from '../../../../utils/get-error-by-type' import { EnvironmentNameLabel } from '../environment-name-label/environment-name-label' import { useFocusTrap } from '../dev-tools-indicator/utils' import { Fader } from '../../fader' +import { Resizer } from '../../resizer' export interface ErrorOverlayLayoutProps extends ErrorBaseProps { errorMessage: ErrorMessageType @@ -59,6 +60,7 @@ export function ErrorOverlayLayout({ errorType, children, errorCode, + errorCount, error, debugInfo, isBuildError, @@ -83,6 +85,10 @@ export function ErrorOverlayLayout({ } as React.CSSProperties, } + const [animating, setAnimating] = React.useState( + Boolean(transitionDurationMs) + ) + const faderRef = React.useRef(null) const hasFooter = Boolean(footerMessage || errorCode) const dialogRef = React.useRef(null) @@ -95,9 +101,23 @@ export function ErrorOverlayLayout({ } } + function onTransitionEnd({ propertyName, target }: React.TransitionEvent) { + // We can only measure height after the `scale` transition ends, + // otherwise we will measure height as a multiple of the animating value + // which will give us an incorrect value. + if (propertyName === 'scale' && target === dialogRef.current) { + setAnimating(false) + } + } + return ( -
+
- - -
- - - {error.environmentName && ( - - )} - - -
- -
+ + + +
+ + + {error.environmentName && ( + + )} + + +
+ +
+ + {children} +
+
- {children} -
diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay/error-overlay.tsx b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay/error-overlay.tsx index bc0bd5e4dd109..468f8a014ca57 100644 --- a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay/error-overlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay/error-overlay.tsx @@ -13,6 +13,7 @@ export interface ErrorBaseProps { transitionDurationMs: number isTurbopack: boolean versionInfo: OverlayState['versionInfo'] + errorCount: number } export function ErrorOverlay({ @@ -20,11 +21,13 @@ export function ErrorOverlay({ runtimeErrors, isErrorOverlayOpen, setIsErrorOverlayOpen, + errorCount, }: { state: OverlayState runtimeErrors: ReadyRuntimeError[] isErrorOverlayOpen: boolean setIsErrorOverlayOpen: (value: boolean) => void + errorCount: number }) { const isTurbopack = !!process.env.TURBOPACK @@ -38,6 +41,7 @@ export function ErrorOverlay({ transitionDurationMs, isTurbopack, versionInfo: state.versionInfo, + errorCount, } if (state.buildError !== null) { diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/resizer/index.tsx b/packages/next/src/client/components/react-dev-overlay/ui/components/resizer/index.tsx new file mode 100644 index 0000000000000..241393090c17e --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/ui/components/resizer/index.tsx @@ -0,0 +1,65 @@ +import { useEffect, useRef, useState } from 'react' + +export function Resizer({ + children, + measure, + ...props +}: { + children: React.ReactNode + measure: boolean +} & React.HTMLProps & { ref?: React.Ref }) { + const ref = useRef(null) + const [height, measuring] = useMeasureHeight(ref, measure) + + return ( +
+
{children}
+
+ ) +} + +function useMeasureHeight( + ref: React.RefObject, + measure: boolean +): [number, boolean] { + const [height, setHeight] = useState(0) + const [measuring, setMeasuring] = useState(true) + + useEffect(() => { + if (!measure) { + return + } + + let timerId: number + const el = ref.current + + if (!el) { + return + } + + const observer = new ResizeObserver(([{ contentRect }]) => { + clearTimeout(timerId) + + timerId = window.setTimeout(() => { + setMeasuring(false) + }, 100) + + setHeight(contentRect.height) + }) + + observer.observe(el) + return () => observer.disconnect() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [measure]) + + return [height, measuring] +} diff --git a/packages/next/src/client/components/react-dev-overlay/ui/container/errors.tsx b/packages/next/src/client/components/react-dev-overlay/ui/container/errors.tsx index 2c7928d881ff2..07c8b361d761d 100644 --- a/packages/next/src/client/components/react-dev-overlay/ui/container/errors.tsx +++ b/packages/next/src/client/components/react-dev-overlay/ui/container/errors.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect, useRef, Suspense } from 'react' +import { useState, useMemo, useRef, Suspense } from 'react' import type { DebugInfo } from '../../types' import { Overlay } from '../components/overlay' import { RuntimeError } from './runtime-error' @@ -89,18 +89,6 @@ export function Errors({ }: ErrorsProps) { const dialogResizerRef = useRef(null) - useEffect(() => { - // Close the error overlay when pressing escape - function handleKeyDown(event: KeyboardEvent) { - if (event.key === 'Escape') { - onClose() - } - } - - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) - }, [onClose]) - const isLoading = useMemo(() => { return runtimeErrors.length < 1 }, [runtimeErrors.length]) diff --git a/packages/next/src/client/components/react-dev-overlay/ui/dev-overlay.stories.tsx b/packages/next/src/client/components/react-dev-overlay/ui/dev-overlay.stories.tsx index 27c9de8a9907e..735cb06313fd6 100644 --- a/packages/next/src/client/components/react-dev-overlay/ui/dev-overlay.stories.tsx +++ b/packages/next/src/client/components/react-dev-overlay/ui/dev-overlay.stories.tsx @@ -92,9 +92,8 @@ export const Default: Story = { src={imgApp} style={{ width: '100%', - height: '100%', + height: '100vh', objectFit: 'contain', - filter: 'invert(1)', }} /> diff --git a/packages/next/src/client/components/react-dev-overlay/ui/hooks/use-measure-height.ts b/packages/next/src/client/components/react-dev-overlay/ui/hooks/use-measure-height.ts deleted file mode 100644 index 54097e8bd4d6c..0000000000000 --- a/packages/next/src/client/components/react-dev-overlay/ui/hooks/use-measure-height.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useState } from 'react' - -export function useMeasureHeight( - ref: React.RefObject -): [number, boolean] { - const [pristine, setPristine] = useState(true) - const [height, setHeight] = useState(0) - - useEffect(() => { - const el = ref.current - - if (!el) { - return - } - - const observer = new ResizeObserver(() => { - const { height: h } = el.getBoundingClientRect() - setHeight((prevHeight) => { - if (prevHeight !== 0) { - setPristine(false) - } - return h - }) - }) - - observer.observe(el) - return () => { - observer.disconnect() - setPristine(true) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return [height, pristine] -} From 92288f149251a19c6eb5fda841d020d0689cbf87 Mon Sep 17 00:00:00 2001 From: rauno Date: Fri, 4 Apr 2025 16:51:48 +0300 Subject: [PATCH 2/2] Update error-overlay-layout.test.tsx --- .../errors/error-overlay-layout/error-overlay-layout.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-layout/error-overlay-layout.test.tsx b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-layout/error-overlay-layout.test.tsx index c17d0de308847..8587fb6fcd824 100644 --- a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-layout/error-overlay-layout.test.tsx +++ b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-layout/error-overlay-layout.test.tsx @@ -24,6 +24,7 @@ const renderTestComponent = () => { rendered={true} transitionDurationMs={200} isTurbopack={false} + errorCount={1} versionInfo={{ installed: '15.0.0', staleness: 'fresh',