Skip to content

Commit

Permalink
Use ErrorOverlayLayout in Errors component (#74107)
Browse files Browse the repository at this point in the history
This PR modified the `Errors` (Runtime Error component) to use the shared `ErrorOverlayLayout`.

![CleanShot 2024-12-20 at 12 24 43@2x](https://github.com/user-attachments/assets/a6d5d224-318c-4d6f-ac46-21636961816a)
  • Loading branch information
devjiwonchoi authored Dec 20, 2024
1 parent 41dbb83 commit 29aa6d1
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 178 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,49 @@
import type { ReadyRuntimeError } from '../../../helpers/get-error-by-type'
import type { DebugInfo } from '../../../../../types'
import type { VersionInfo } from '../../../../../../../../server/dev/parse-version-info'
import { Dialog, DialogHeader, DialogBody, DialogContent } from '../../Dialog'
import { Overlay } from '../../Overlay'
import { ErrorPagination } from '../ErrorPagination/ErrorPagination'
import { ToolButtonsGroup } from '../../ToolButtonsGroup/ToolButtonsGroup'
import { VersionStalenessInfo } from '../../VersionStalenessInfo'

type ErrorOverlayLayoutProps = {
errorMessage: string | React.ReactNode
errorType:
| 'Build Error'
| 'Runtime Error'
| 'Console Error'
| 'Unhandled Runtime Error'
| 'Missing Required HTML Tag'
errorMessage: string | React.ReactNode
onClose: () => void
children?: React.ReactNode
errorCode?: string
error?: Error
debugInfo?: DebugInfo
isBuildError?: boolean
onClose?: () => void
// TODO: remove this
temporaryHeaderChildren?: React.ReactNode
versionInfo?: VersionInfo
children?: React.ReactNode
// TODO: better handle receiving
readyErrors?: ReadyRuntimeError[]
activeIdx?: number
setActiveIndex?: (index: number) => void
}

export function ErrorOverlayLayout({
errorType,
errorMessage,
onClose,
errorType,
children,
versionInfo,
errorCode,
error,
debugInfo,
isBuildError,
onClose,
temporaryHeaderChildren,
versionInfo,
readyErrors,
activeIdx,
setActiveIndex,
}: ErrorOverlayLayoutProps) {
return (
<Overlay fixed={isBuildError}>
Expand All @@ -35,19 +55,34 @@ export function ErrorOverlayLayout({
>
<DialogContent>
<DialogHeader className="nextjs-container-errors-header">
<h1
id="nextjs__container_errors_label"
className="nextjs__container_errors_label"
{/* TODO: better passing data instead of nullish coalescing */}
<ErrorPagination
readyErrors={readyErrors ?? []}
activeIdx={activeIdx ?? 0}
onActiveIndexChange={setActiveIndex ?? (() => {})}
/>
<div
className="nextjs__container_errors__error_title"
// allow assertion in tests before error rating is implemented
data-nextjs-error-code={errorCode}
>
{errorType}
</h1>
<h1
id="nextjs__container_errors_label"
className="nextjs__container_errors_label"
>
{errorType}
{/* TODO: Need to relocate this so consider data flow. */}
</h1>
<ToolButtonsGroup error={error} debugInfo={debugInfo} />
</div>
<VersionStalenessInfo versionInfo={versionInfo} />
<p
id="nextjs__container_errors_desc"
className="nextjs__container_errors_desc"
>
{errorMessage}
</p>
{temporaryHeaderChildren}
</DialogHeader>
<DialogBody className="nextjs-container-errors-body">
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react'
import { ErrorPagination } from './ErrorPagination'
import { withShadowPortal } from '../../../storybook/with-shadow-portal'
import { useState } from 'react'

const meta: Meta<typeof ErrorPagination> = {
title: 'ErrorPagination',
Expand Down Expand Up @@ -37,45 +38,40 @@ const mockErrors = [
]

export const SingleError: Story = {
args: {
activeIdx: 0,
previous: () => console.log('Previous clicked'),
next: () => console.log('Next clicked'),
readyErrors: [mockErrors[0]],
minimize: () => console.log('Minimize clicked'),
isServerError: false,
render: function ErrorPaginationStory() {
const [activeIdx, setActiveIdx] = useState(0)
return (
<ErrorPagination
activeIdx={activeIdx}
readyErrors={[mockErrors[0]]}
onActiveIndexChange={setActiveIdx}
/>
)
},
}

export const MultipleErrors: Story = {
args: {
activeIdx: 1,
previous: () => console.log('Previous clicked'),
next: () => console.log('Next clicked'),
readyErrors: mockErrors,
minimize: () => console.log('Minimize clicked'),
isServerError: false,
render: function ErrorPaginationStory() {
const [activeIdx, setActiveIdx] = useState(1)
return (
<ErrorPagination
activeIdx={activeIdx}
readyErrors={mockErrors}
onActiveIndexChange={setActiveIdx}
/>
)
},
}

export const LastError: Story = {
args: {
activeIdx: 2,
previous: () => console.log('Previous clicked'),
next: () => console.log('Next clicked'),
readyErrors: mockErrors,
minimize: () => console.log('Minimize clicked'),
isServerError: false,
},
}

export const ServerError: Story = {
args: {
activeIdx: 0,
previous: () => console.log('Previous clicked'),
next: () => console.log('Next clicked'),
readyErrors: [mockErrors[0]],
minimize: () => console.log('Minimize clicked'),
isServerError: true,
render: function ErrorPaginationStory() {
const [activeIdx, setActiveIdx] = useState(2)
return (
<ErrorPagination
activeIdx={activeIdx}
readyErrors={mockErrors}
onActiveIndexChange={setActiveIdx}
/>
)
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@ import { LeftArrow } from '../../../icons/LeftArrow'
import { RightArrow } from '../../../icons/RightArrow'

type ErrorPaginationProps = {
activeIdx: number
previous: () => void
next: () => void
readyErrors: ReadyRuntimeError[]
minimize: () => void
isServerError: boolean
activeIdx: number
onActiveIndexChange: (index: number) => void
}

export function ErrorPagination({
activeIdx,
previous,
next,
readyErrors,
minimize,
isServerError,
activeIdx,
onActiveIndexChange,
}: ErrorPaginationProps) {
const previousHandler = activeIdx > 0 ? previous : null
const nextHandler = activeIdx < readyErrors.length - 1 ? next : null
const close = isServerError ? undefined : minimize
const handlePrevious = useCallback(
() =>
activeIdx > 0 ? onActiveIndexChange(Math.max(0, activeIdx - 1)) : null,
[activeIdx, onActiveIndexChange]
)

const handleNext = useCallback(
() =>
activeIdx < readyErrors.length - 1
? onActiveIndexChange(
Math.max(0, Math.min(readyErrors.length - 1, activeIdx + 1))
)
: null,
[activeIdx, readyErrors.length, onActiveIndexChange]
)

const buttonLeft = useRef<HTMLButtonElement | null>(null)
const buttonRight = useRef<HTMLButtonElement | null>(null)
Expand All @@ -48,14 +54,14 @@ export function ErrorPagination({
if (buttonLeft.current) {
buttonLeft.current.focus()
}
previousHandler && previousHandler()
handlePrevious && handlePrevious()
} else if (e.key === 'ArrowRight') {
e.preventDefault()
e.stopPropagation()
if (buttonRight.current) {
buttonRight.current.focus()
}
nextHandler && nextHandler()
handleNext && handleNext()
} else if (e.key === 'Escape') {
e.preventDefault()
e.stopPropagation()
Expand All @@ -81,7 +87,7 @@ export function ErrorPagination({
d.removeEventListener('keydown', handler)
}
}
}, [close, nav, nextHandler, previousHandler])
}, [nav, handleNext, handlePrevious])

// Unlock focus for browsers like Firefox, that break all user focus if the
// currently focused item becomes disabled.
Expand All @@ -95,36 +101,36 @@ export function ErrorPagination({
if (root instanceof ShadowRoot) {
const a = root.activeElement

if (previousHandler == null) {
if (activeIdx === 0) {
if (buttonLeft.current && a === buttonLeft.current) {
buttonLeft.current.blur()
}
} else if (nextHandler == null) {
} else if (activeIdx === readyErrors.length - 1) {
if (buttonRight.current && a === buttonRight.current) {
buttonRight.current.blur()
}
}
}
}, [nav, nextHandler, previousHandler])
}, [nav, activeIdx, readyErrors.length])

return (
<div data-nextjs-dialog-left-right>
<nav ref={onNav}>
<button
ref={buttonLeft}
type="button"
disabled={previousHandler == null ? true : undefined}
aria-disabled={previousHandler == null ? true : undefined}
onClick={previousHandler ?? undefined}
disabled={activeIdx === 0}
aria-disabled={activeIdx === 0}
onClick={handlePrevious}
>
<LeftArrow title="previous" />
</button>
<button
ref={buttonRight}
type="button"
disabled={nextHandler == null ? true : undefined}
aria-disabled={nextHandler == null ? true : undefined}
onClick={nextHandler ?? undefined}
disabled={activeIdx === readyErrors.length - 1}
aria-disabled={activeIdx === readyErrors.length - 1}
onClick={handleNext}
>
<RightArrow title="next" />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CopyButton } from '../copy-button'
import { NodejsInspectorCopyButton } from '../nodejs-inspector'

type ToolButtonsGroupProps = {
error: Error
error: Error | undefined
debugInfo: DebugInfo | undefined
}

Expand All @@ -14,8 +14,8 @@ export function ToolButtonsGroup({ error, debugInfo }: ToolButtonsGroupProps) {
data-nextjs-data-runtime-error-copy-stack
actionLabel="Copy error stack"
successLabel="Copied"
content={error.stack || ''}
disabled={!error.stack}
content={error?.stack || ''}
disabled={!error?.stack}
/>
<NodejsInspectorCopyButton
devtoolsFrontendUrl={debugInfo?.devtoolsFrontendUrl}
Expand Down
Loading

0 comments on commit 29aa6d1

Please sign in to comment.