From b40e1a2d4f1ef4a0869e8189770ac5f2d806d83c Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Fri, 20 Dec 2024 23:12:38 +0900 Subject: [PATCH] Port ErrorPagination (#74097) Ported `ErrorPagination` and replaced in the `Errors` component. --- .../ErrorPagination.stories.tsx | 81 +++++++++ .../ErrorPagination/ErrorPagination.tsx | 166 +++++++++++++++++ .../LeftRightDialogHeader.tsx | 169 ------------------ .../components/LeftRightDialogHeader/index.ts | 2 - .../LeftRightDialogHeader/styles.ts | 69 ------- .../internal/container/Errors.tsx | 26 ++- .../internal/styles/ComponentStyles.tsx | 2 - 7 files changed, 257 insertions(+), 258 deletions(-) create mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/ErrorPagination/ErrorPagination.stories.tsx create mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/ErrorPagination/ErrorPagination.tsx delete mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/LeftRightDialogHeader.tsx delete mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/index.ts delete mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/styles.ts diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/ErrorPagination/ErrorPagination.stories.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/ErrorPagination/ErrorPagination.stories.tsx new file mode 100644 index 0000000000000..2c1eb8da94ec8 --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/ErrorPagination/ErrorPagination.stories.tsx @@ -0,0 +1,81 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { ErrorPagination } from './ErrorPagination' +import { withShadowPortal } from '../../../storybook/with-shadow-portal' + +const meta: Meta = { + title: 'ErrorPagination', + component: ErrorPagination, + parameters: { + layout: 'centered', + }, + decorators: [withShadowPortal], +} + +export default meta +type Story = StoryObj + +// Mock errors for stories +const mockErrors = [ + { + id: 1, + runtime: true as const, + error: new Error('First error'), + frames: [], + }, + { + id: 2, + runtime: true as const, + error: new Error('Second error'), + frames: [], + }, + { + id: 3, + runtime: true as const, + error: new Error('Third error'), + frames: [], + }, +] + +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, + }, +} + +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, + }, +} + +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, + }, +} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/ErrorPagination/ErrorPagination.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/ErrorPagination/ErrorPagination.tsx new file mode 100644 index 0000000000000..fa8d43ccc13f9 --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/ErrorPagination/ErrorPagination.tsx @@ -0,0 +1,166 @@ +import type { ReadyRuntimeError } from '../../../helpers/get-error-by-type' +import { useCallback, useEffect, useRef, useState } from 'react' + +type ErrorPaginationProps = { + activeIdx: number + previous: () => void + next: () => void + readyErrors: ReadyRuntimeError[] + minimize: () => void + isServerError: boolean +} + +export function ErrorPagination({ + activeIdx, + previous, + next, + readyErrors, + minimize, + isServerError, +}: ErrorPaginationProps) { + const previousHandler = activeIdx > 0 ? previous : null + const nextHandler = activeIdx < readyErrors.length - 1 ? next : null + const close = isServerError ? undefined : minimize + + const buttonLeft = useRef(null) + const buttonRight = useRef(null) + const buttonClose = useRef(null) + + const [nav, setNav] = useState(null) + const onNav = useCallback((el: HTMLElement) => { + setNav(el) + }, []) + + useEffect(() => { + if (nav == null) { + return + } + + const root = nav.getRootNode() + const d = self.document + + function handler(e: KeyboardEvent) { + if (e.key === 'ArrowLeft') { + e.preventDefault() + e.stopPropagation() + if (buttonLeft.current) { + buttonLeft.current.focus() + } + previousHandler && previousHandler() + } else if (e.key === 'ArrowRight') { + e.preventDefault() + e.stopPropagation() + if (buttonRight.current) { + buttonRight.current.focus() + } + nextHandler && nextHandler() + } else if (e.key === 'Escape') { + e.preventDefault() + e.stopPropagation() + if (root instanceof ShadowRoot) { + const a = root.activeElement + if (a && a !== buttonClose.current && a instanceof HTMLElement) { + a.blur() + return + } + } + + close?.() + } + } + + root.addEventListener('keydown', handler as EventListener) + if (root !== d) { + d.addEventListener('keydown', handler) + } + return function () { + root.removeEventListener('keydown', handler as EventListener) + if (root !== d) { + d.removeEventListener('keydown', handler) + } + } + }, [close, nav, nextHandler, previousHandler]) + + // Unlock focus for browsers like Firefox, that break all user focus if the + // currently focused item becomes disabled. + useEffect(() => { + if (nav == null) { + return + } + + const root = nav.getRootNode() + // Always true, but we do this for TypeScript: + if (root instanceof ShadowRoot) { + const a = root.activeElement + + if (previousHandler == null) { + if (buttonLeft.current && a === buttonLeft.current) { + buttonLeft.current.blur() + } + } else if (nextHandler == null) { + if (buttonRight.current && a === buttonRight.current) { + buttonRight.current.blur() + } + } + } + }, [nav, nextHandler, previousHandler]) + + return ( +
+ +
+ ) +} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/LeftRightDialogHeader.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/LeftRightDialogHeader.tsx deleted file mode 100644 index 3db38e9967a2d..0000000000000 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/LeftRightDialogHeader.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import * as React from 'react' -import { CloseIcon } from '../../icons/CloseIcon' - -export type LeftRightDialogHeaderProps = { - children?: React.ReactNode - className?: string - previous: (() => void) | null - next: (() => void) | null - close?: () => void -} - -const LeftRightDialogHeader: React.FC = - function LeftRightDialogHeader({ - children, - className, - previous, - next, - close, - }) { - const buttonLeft = React.useRef(null) - const buttonRight = React.useRef(null) - const buttonClose = React.useRef(null) - - const [nav, setNav] = React.useState(null) - const onNav = React.useCallback((el: HTMLElement) => { - setNav(el) - }, []) - - React.useEffect(() => { - if (nav == null) { - return - } - - const root = nav.getRootNode() - const d = self.document - - function handler(e: KeyboardEvent) { - if (e.key === 'ArrowLeft') { - e.preventDefault() - e.stopPropagation() - if (buttonLeft.current) { - buttonLeft.current.focus() - } - previous && previous() - } else if (e.key === 'ArrowRight') { - e.preventDefault() - e.stopPropagation() - if (buttonRight.current) { - buttonRight.current.focus() - } - next && next() - } else if (e.key === 'Escape') { - e.preventDefault() - e.stopPropagation() - if (root instanceof ShadowRoot) { - const a = root.activeElement - if (a && a !== buttonClose.current && a instanceof HTMLElement) { - a.blur() - return - } - } - - close?.() - } - } - - root.addEventListener('keydown', handler as EventListener) - if (root !== d) { - d.addEventListener('keydown', handler) - } - return function () { - root.removeEventListener('keydown', handler as EventListener) - if (root !== d) { - d.removeEventListener('keydown', handler) - } - } - }, [close, nav, next, previous]) - - // Unlock focus for browsers like Firefox, that break all user focus if the - // currently focused item becomes disabled. - React.useEffect(() => { - if (nav == null) { - return - } - - const root = nav.getRootNode() - // Always true, but we do this for TypeScript: - if (root instanceof ShadowRoot) { - const a = root.activeElement - - if (previous == null) { - if (buttonLeft.current && a === buttonLeft.current) { - buttonLeft.current.blur() - } - } else if (next == null) { - if (buttonRight.current && a === buttonRight.current) { - buttonRight.current.blur() - } - } - } - }, [nav, next, previous]) - - return ( -
- - {close ? ( - - ) : null} -
- ) - } - -export { LeftRightDialogHeader } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/index.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/index.ts deleted file mode 100644 index b63edb12ba22a..0000000000000 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { LeftRightDialogHeader } from './LeftRightDialogHeader' -export { styles } from './styles' diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/styles.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/styles.ts deleted file mode 100644 index 38762cf5bc70a..0000000000000 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/LeftRightDialogHeader/styles.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { noop as css } from '../../helpers/noop-template' - -const styles = css` - [data-nextjs-dialog-left-right] { - display: flex; - flex-direction: row; - align-content: center; - align-items: center; - justify-content: space-between; - } - [data-nextjs-dialog-left-right] > nav { - flex: 1; - display: flex; - align-items: center; - margin-right: var(--size-gap); - } - [data-nextjs-dialog-left-right] > nav > button { - display: inline-flex; - align-items: center; - justify-content: center; - - width: calc(var(--size-gap-double) + var(--size-gap)); - height: calc(var(--size-gap-double) + var(--size-gap)); - font-size: 0; - border: none; - background-color: rgba(255, 85, 85, 0.1); - color: var(--color-ansi-red); - cursor: pointer; - transition: background-color 0.25s ease; - } - [data-nextjs-dialog-left-right] > nav > button > svg { - width: auto; - height: calc(var(--size-gap) + var(--size-gap-half)); - } - [data-nextjs-dialog-left-right] > nav > button:hover { - background-color: rgba(255, 85, 85, 0.2); - } - [data-nextjs-dialog-left-right] > nav > button:disabled { - background-color: rgba(255, 85, 85, 0.1); - color: rgba(255, 85, 85, 0.4); - cursor: not-allowed; - } - - [data-nextjs-dialog-left-right] > nav > button:first-of-type { - border-radius: var(--size-gap-half) 0 0 var(--size-gap-half); - margin-right: 1px; - } - [data-nextjs-dialog-left-right] > nav > button:last-of-type { - border-radius: 0 var(--size-gap-half) var(--size-gap-half) 0; - } - - [data-nextjs-dialog-left-right] > button:last-of-type { - border: 0; - padding: 0; - - background-color: transparent; - appearance: none; - - opacity: 0.4; - transition: opacity 0.25s ease; - - color: var(--color-font); - } - [data-nextjs-dialog-left-right] > button:last-of-type:hover { - opacity: 0.7; - } -` - -export { styles } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/Errors.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/Errors.tsx index bdc95c1b4e6dc..683811c27042f 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/Errors.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/Errors.tsx @@ -12,7 +12,6 @@ import { DialogContent, DialogHeader, } from '../components/Dialog' -import { LeftRightDialogHeader } from '../components/LeftRightDialogHeader' import { Overlay } from '../components/Overlay' import { getErrorByType } from '../helpers/get-error-by-type' import type { ReadyRuntimeError } from '../helpers/get-error-by-type' @@ -35,6 +34,7 @@ import { } from '../helpers/console-error' import { extractNextErrorCode } from '../../../../../../lib/error-telemetry-utils' import { ErrorIndicator } from '../components/Errors/ErrorIndicator/ErrorIndicator' +import { ErrorPagination } from '../components/Errors/ErrorPagination/ErrorPagination' export type SupportedErrorEvent = { id: number @@ -273,21 +273,15 @@ export function Errors({ > - 0 ? previous : null} - next={activeIdx < readyErrors.length - 1 ? next : null} - close={isServerError ? undefined : minimize} - > - - {activeIdx + 1} of{' '} - - {readyErrors.length} - - {' issue'} - {readyErrors.length < 2 ? '' : 's'} - - - + +