diff --git a/pages/app/index.tsx b/pages/app/index.tsx index 9c3d2776b0..555bfd09a4 100644 --- a/pages/app/index.tsx +++ b/pages/app/index.tsx @@ -48,6 +48,7 @@ function isAppLayoutPage(pageId?: string) { 'prompt-input/simple', 'funnel-analytics/static-single-page-flow', 'funnel-analytics/static-multi-page-flow', + 'error-boundary', ]; return pageId !== undefined && appLayoutPages.some(match => pageId.includes(match)); } diff --git a/pages/error-boundary/demo-async-load.page.tsx b/pages/error-boundary/demo-async-load.page.tsx new file mode 100644 index 0000000000..b41bd22387 --- /dev/null +++ b/pages/error-boundary/demo-async-load.page.tsx @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { Suspense } from 'react'; + +import { AppLayout, Spinner } from '~components'; +import { ErrorBoundariesProvider } from '~components/error-boundary/context'; +import I18nProvider from '~components/i18n'; +import messages from '~components/i18n/messages/all.en'; + +import ScreenshotArea from '../utils/screenshot-area'; + +function createDelayedResource(ms: number, error: Error) { + let done = false; + const promise = new Promise(resolve => + setTimeout(() => { + done = true; + resolve(); + }, ms) + ); + return { + read() { + if (!done) { + throw promise; + } + throw error; + }, + }; +} + +const resource = createDelayedResource(2000, new Error('Async page load failed')); + +function AsyncFailingPage() { + resource.read(); + return
Loaded page
; +} + +export default function ErrorBoundaryAsyncDemo() { + return ( + + + + {/* AppLayout remains synchronous */} + }> + + + } + /> + + + + ); +} + +function Fallback() { + return ( +
+ +
+ ); +} diff --git a/pages/error-boundary/demo-components.page.tsx b/pages/error-boundary/demo-components.page.tsx new file mode 100644 index 0000000000..f3b4461861 --- /dev/null +++ b/pages/error-boundary/demo-components.page.tsx @@ -0,0 +1,147 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useContext, useState } from 'react'; + +import { + AppLayout, + Box, + Button, + Checkbox, + Container, + Drawer, + ExpandableSection, + Header, + Link, + Modal, + Popover, + SpaceBetween, + SplitPanel, + Table, +} from '~components'; +import { ErrorBoundariesProvider } from '~components/error-boundary/context'; +import I18nProvider from '~components/i18n'; +import messages from '~components/i18n/messages/all.en'; + +import AppContext, { AppContextType } from '../app/app-context'; +import ScreenshotArea from '../utils/screenshot-area'; + +type PageContext = React.Context>; + +export default function () { + const { + urlParams: { errorBoundariesActive = true }, + setUrlParams, + } = useContext(AppContext as PageContext); + const [splitPanelOpen, setSplitPanelOpen] = useState(true); + const [activeDrawerId, setActiveDrawerId] = useState('d1'); + const [modalOpen, setModalOpen] = useState(false); + return ( + + + + setActiveDrawerId(detail.activeDrawerId)} + splitPanel={ + + + + } + splitPanelOpen={splitPanelOpen} + onSplitPanelToggle={({ detail }) => setSplitPanelOpen(detail.open)} + drawers={[ + { + id: 'd1', + content: ( + Drawer 1}> + + + ), + trigger: { iconName: 'bug' }, + ariaLabels: { drawerName: 'Drawer 1', triggerButton: 'Open drawer 1', closeButton: 'Close drawer 1' }, + }, + { + id: 'd2', + content: ( + Drawer 2}> + + + ), + trigger: { iconName: 'call' }, + ariaLabels: { drawerName: 'Drawer 2', triggerButton: 'Open drawer 2', closeButton: 'Close drawer 2' }, + }, + ]} + content={ + +

Error boundary demo: components

+ + setUrlParams({ errorBoundariesActive: detail.checked })} + > + Error boundaries on + + + + + + + + + + setModalOpen(false)}> + + + + + } triggerType="custom"> + + + + + Container 1}> + + + + Container 2}> + + + + } + defaultExpanded={true} + > + + + + Table} + columnDefinitions={[ + { + header: 'Column 1', + cell: item => {item}, + }, + { header: 'Column 2', cell: item => `Content 2:${item}` }, + { header: 'Column 3', cell: item => `Content 3:${item}` }, + { header: 'Actions', cell: () => }, + ]} + items={[1, 2, 3]} + >
+
+
+ } + /> +
+
+
+ ); +} + +function BrokenButton() { + const [errorState, setErrorState] = useState(false); + return ; +} diff --git a/pages/error-boundary/demo-form.page.tsx b/pages/error-boundary/demo-form.page.tsx new file mode 100644 index 0000000000..3f2e23921f --- /dev/null +++ b/pages/error-boundary/demo-form.page.tsx @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState } from 'react'; + +import { AppLayout, Box, Button, Container, ExpandableSection, Form, Header, Popover, SpaceBetween } from '~components'; +import { ErrorBoundariesProvider } from '~components/error-boundary/context'; +import I18nProvider from '~components/i18n'; +import messages from '~components/i18n/messages/all.en'; + +import ScreenshotArea from '../utils/screenshot-area'; + +export default function () { + return ( + + + + +

Error boundary demo: form

+

When an unexpected error occurs inside a form, it is no longer scoped to sections.

+ +
+ + + } + > + + Container 1}> + + + + Container 2}> + } triggerType="custom"> + + + + + } + defaultExpanded={true} + > + + + + + + } + /> +
+
+
+ ); +} + +function BrokenButton() { + const [errorState, setErrorState] = useState(false); + return ; +} diff --git a/src/app-layout/error-boundary.tsx b/src/app-layout/error-boundary.tsx new file mode 100644 index 0000000000..f3879eeb0b --- /dev/null +++ b/src/app-layout/error-boundary.tsx @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import InternalErrorBoundary from '../error-boundary/internal'; + +import styles from './styles.css.js'; + +export function ErrorBoundaryMain({ children }: { children: React.ReactNode }) { + return ( +
{content}
}> + {children} +
+ ); +} diff --git a/src/app-layout/styles.scss b/src/app-layout/styles.scss index c39b1e1cc8..2464a2ab8c 100644 --- a/src/app-layout/styles.scss +++ b/src/app-layout/styles.scss @@ -35,6 +35,7 @@ min-inline-size: 0; background-color: awsui.$color-background-layout-main; position: relative; + &-scrollable { overflow: auto; } @@ -68,3 +69,10 @@ // applied to content or content header, whatever comes first padding-block-start: awsui.$space-scaled-m; } + +.error-boundary-wrapper { + block-size: 100%; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx index 6374977d38..7e7bcf2ddf 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx @@ -128,6 +128,7 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD />
{activeDrawerId !== TOOLS_DRAWER_ID && ( -
+
{activeDrawer?.content}
)} diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index 7df2e99b24..01c3d79bb5 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -8,6 +8,7 @@ import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-tool import { GeneratedAnalyticsMetadataAppLayoutToolbarComponent } from '../../../app-layout-toolbar/analytics-metadata/interfaces'; import VisualContext from '../../../internal/components/visual-context'; import customCssProps from '../../../internal/generated/custom-css-properties'; +import { ErrorBoundaryMain } from '../../error-boundary'; import { AppLayoutInternalProps, AppLayoutPendingState } from '../interfaces'; import { AppLayoutAfterMainSlot, @@ -86,7 +87,7 @@ export const SkeletonLayout = ({ {contentHeader &&
{contentHeader}
} {/*delay rendering the content until registration of this instance is complete*/}
- {registered ? content : null} + {registered ? {content} : null}
diff --git a/src/app-layout/visual-refresh/drawers.tsx b/src/app-layout/visual-refresh/drawers.tsx index e9acb21623..c4ed58e5bd 100644 --- a/src/app-layout/visual-refresh/drawers.tsx +++ b/src/app-layout/visual-refresh/drawers.tsx @@ -143,6 +143,7 @@ function ActiveDrawer() {
{toolsContent && (
)} {activeDrawerId !== TOOLS_DRAWER_ID && ( -
{activeDrawerId && activeDrawer?.content}
+
+ {activeDrawerId && activeDrawer?.content} +
)}
diff --git a/src/app-layout/visual-refresh/main.tsx b/src/app-layout/visual-refresh/main.tsx index 56ca4361fd..af3a560b74 100644 --- a/src/app-layout/visual-refresh/main.tsx +++ b/src/app-layout/visual-refresh/main.tsx @@ -5,6 +5,7 @@ import clsx from 'clsx'; import customCssProps from '../../internal/generated/custom-css-properties'; import * as tokens from '../../internal/generated/styles/tokens'; +import { ErrorBoundaryMain } from '../error-boundary'; import { getStickyOffsetVars } from '../utils/sticky-offsets'; import { useAppLayoutInternals } from './context'; @@ -63,7 +64,7 @@ export default function Main() { ), }} > - {content} + {content} ); } diff --git a/src/container/internal.tsx b/src/container/internal.tsx index 32ce40117c..c222809835 100644 --- a/src/container/internal.tsx +++ b/src/container/internal.tsx @@ -1,11 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + import React, { useRef } from 'react'; import clsx from 'clsx'; import { useMergeRefs, useUniqueId } from '@cloudscape-design/component-toolkit/internal'; import { getAnalyticsLabelAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; +import InternalBox from '../box/internal'; +import InternalErrorBoundary from '../error-boundary/internal'; import { useFunnelSubStep } from '../internal/analytics/hooks/use-funnel'; import { getBaseProps } from '../internal/base-component'; import { ContainerHeaderContextProvider } from '../internal/context/container-header'; @@ -145,60 +148,68 @@ export default function InternalContainer({ ref={__subStepRef} className={clsx(styles['content-wrapper'], fitHeight && styles['content-wrapper-fit-height'])} > - {header && ( - - -
{content}}> + {header && ( + + +
+ {isStuck && !isMobile && isRefresh && __fullPage &&
} + {header} +
+
+
+ )} +
+
+ ( + {content} )} - ref={headerMergedRef} - style={{ - ...stickyStyles.style, - ...getHeaderStyles(style), - }} > - {isStuck && !isMobile && isRefresh && __fullPage &&
} - {header} -
- - - )} -
-
- {children} -
-
- {footer && ( -
- {footer} + {children} + +
- )} + {footer && ( +
+ {footer} +
+ )} +
); diff --git a/src/drawer/implementation.tsx b/src/drawer/implementation.tsx index a27ba5abad..df3bd97d21 100644 --- a/src/drawer/implementation.tsx +++ b/src/drawer/implementation.tsx @@ -5,6 +5,7 @@ import clsx from 'clsx'; import { useRuntimeDrawerContext } from '../app-layout/runtime-drawer/use-runtime-drawer-context'; import { useAppLayoutToolbarDesignEnabled } from '../app-layout/utils/feature-flags'; +import InternalErrorBoundary from '../error-boundary/internal'; import { useInternalI18n } from '../i18n/context'; import { getBaseProps } from '../internal/base-component'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; @@ -36,7 +37,7 @@ export function DrawerImplementation({ }; const runtimeDrawerContext = useRuntimeDrawerContext({ rootRef: __internalRootRef as RefObject }); - const hasAdditioalDrawerAction = !!runtimeDrawerContext?.isExpandable; + const hasAdditionalDrawerAction = !!runtimeDrawerContext?.isExpandable; return loading ? (
{header} @@ -70,7 +71,7 @@ export function DrawerImplementation({ !disableContentPaddings && styles['content-with-paddings'] )} > - {children} + {children}
); diff --git a/src/error-boundary/context.tsx b/src/error-boundary/context.tsx new file mode 100644 index 0000000000..b3ebc3247a --- /dev/null +++ b/src/error-boundary/context.tsx @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { createContext } from 'react'; + +import { ErrorBoundaryProps } from './interfaces'; + +interface ErrorBoundariesContextValue { + customMessage?: ErrorBoundaryProps.CustomMessage; + feedbackLink?: string; + i18nStrings?: ErrorBoundaryProps.I18nStrings; +} + +export const ErrorBoundariesContext = createContext({ + errorBoundariesActive: false, +}); + +export function ErrorBoundariesProvider({ + children, + active = true, + value, +}: { + children: React.ReactNode; + active?: boolean; + value?: ErrorBoundariesContextValue; +}) { + return ( + + {children} + + ); +} diff --git a/src/error-boundary/index.tsx b/src/error-boundary/index.tsx new file mode 100644 index 0000000000..1201f3a998 --- /dev/null +++ b/src/error-boundary/index.tsx @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +'use client'; +import React from 'react'; + +import { ErrorBoundaryProps } from './interfaces'; +import InternalErrorBoundary from './internal'; + +export { ErrorBoundaryProps }; + +export default function ErrorBoundary(props: ErrorBoundaryProps) { + return ; +} diff --git a/src/error-boundary/interfaces.ts b/src/error-boundary/interfaces.ts new file mode 100644 index 0000000000..0d8327f4cc --- /dev/null +++ b/src/error-boundary/interfaces.ts @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export interface ErrorBoundaryProps { + children: React.ReactNode; + + wrapper?: (content: React.ReactNode) => React.ReactNode; + + customMessage?: ErrorBoundaryProps.CustomMessage; + + feedbackLink?: string; + + i18nStrings?: ErrorBoundaryProps.I18nStrings; +} + +export namespace ErrorBoundaryProps { + export interface CustomMessage { + image?: React.ReactNode; + header?: React.ReactNode; + description?: React.ReactNode; + feedback?: React.ReactNode; + } + + export interface I18nStrings { + imageAltText?: string; + messageHeader?: string; + messageDescription?: string; + messageFeedback?: string; + } +} diff --git a/src/error-boundary/internal.tsx b/src/error-boundary/internal.tsx new file mode 100644 index 0000000000..11c273e757 --- /dev/null +++ b/src/error-boundary/internal.tsx @@ -0,0 +1,199 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { Component, useContext } from 'react'; + +import InternalBox from '../box/internal'; +import InternalButton from '../button/internal'; +import { useInternalI18n } from '../i18n/context'; +import InternalLink from '../link/internal'; +import InternalSpaceBetween from '../space-between/internal'; +import { ErrorBoundariesContext } from './context'; +import { ErrorBoundaryProps } from './interfaces'; + +interface ErrorBoundaryState { + hasError: boolean; +} + +export default function InternalErrorBoundary({ children, ...props }: ErrorBoundaryProps) { + const { errorBoundariesActive, ...context } = useContext(ErrorBoundariesContext); + return errorBoundariesActive ? ( + + {children} + + ) : ( + <>{children} + ); +} + +class ErrorBoundaryImpl extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + render() { + if (this.state.hasError) { + return ; + } + return this.props.children; + } +} + +function ErrorBoundaryFallback(props: ErrorBoundaryProps) { + const image = props.customMessage?.image ?? ; + + const defaultHeaderContent = useDefaultHeaderContent(props); + const header = props.customMessage?.header ?? {defaultHeaderContent}; + + const defaultDescriptionContent = useDefaultDescriptionContent(props); + const description = props.customMessage?.description ?? {defaultDescriptionContent}; + + const defaultFeedbackContent = useDefaultFeedbackContent(props); + const feedback = + props.customMessage?.feedback ?? (props.feedbackLink ? {defaultFeedbackContent} : null); + + const content = ( + + <>{image} + <>{header} + <>{description} + <>{feedback} + + ); + + return <>{props.wrapper?.(content) ?? content}; +} + +function DefaultImage() { + return ( + + ); +} + +function useDefaultHeaderContent({ i18nStrings }: ErrorBoundaryProps) { + const i18n = useInternalI18n('error-boundary'); + return i18n('i18nStrings.messageHeader', i18nStrings?.messageHeader ?? 'There was an unexpected issue'); +} + +function useDefaultDescriptionContent({ feedbackLink, i18nStrings }: ErrorBoundaryProps) { + const i18n = useInternalI18n('error-boundary'); + const parsed = parseBracketLinks( + i18n( + 'i18nStrings.messageDescription', + i18nStrings?.messageDescription ?? 'An unexpected issue led to a crash. [Refresh](refresh) the page to try again.' + ) + ); + return ; +} + +function useDefaultFeedbackContent({ feedbackLink, i18nStrings }: ErrorBoundaryProps) { + const i18n = useInternalI18n('error-boundary'); + if (!feedbackLink) { + return null; + } + const parsed = parseBracketLinks( + i18n( + 'i18nStrings.messageFeedback', + i18nStrings?.messageFeedback ?? 'If the issue persists, please provide us feedback [here](feedback).' + ) + ); + return ; +} + +type ParsedMessage = { type: 'text'; text: string } | { type: 'link'; text: string; value: string }; + +function MessageWithLinks({ parsed, feedbackLink }: { parsed: ParsedMessage[]; feedbackLink?: string }) { + return ( + <> + {parsed.map((chunk, index) => { + if (chunk.type === 'link' && chunk.value === 'refresh') { + return ( + window.location.reload()}> + {chunk.text} + + ); + } else if (chunk.type === 'link' && chunk.value === 'feedback' && feedbackLink) { + return ( + + {chunk.text} + + ); + } else if (chunk.type === 'link' && chunk.value) { + return ( + + {chunk.text} + + ); + } else { + return ( + + {chunk.text} + + ); + } + })} + + ); +} + +function parseBracketLinks(input: string) { + const chunks: ParsedMessage[] = []; + const regex = /\[([^\]]+)\]\(([^)]+)\)/g; // [text](value) + let last = 0; + let match; + + while ((match = regex.exec(input)) !== null) { + // Text before the link. + if (match.index > last) { + chunks.push({ type: 'text', text: input.slice(last, match.index) }); + } + // Link chunk. + chunks.push({ type: 'link', text: match[1], value: match[2] }); + last = regex.lastIndex; + } + // Text after the link. + if (last < input.length) { + chunks.push({ type: 'text', text: input.slice(last) }); + } + return chunks; +} diff --git a/src/form/internal.tsx b/src/form/internal.tsx index c48f2c723e..e52566eeaa 100644 --- a/src/form/internal.tsx +++ b/src/form/internal.tsx @@ -7,6 +7,7 @@ import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-tool import InternalAlert from '../alert/internal'; import InternalBox from '../box/internal'; +import { ErrorBoundariesProvider } from '../error-boundary/context'; import { useInternalI18n } from '../i18n/context'; import { getBaseProps } from '../internal/base-component'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; @@ -48,36 +49,38 @@ export default function InternalForm({ }; return ( -
- {header &&
{header}
} - {children &&
{children}
} - {errorText && ( - - -
- {errorText} + +
+ {header &&
{header}
} + {children &&
{children}
} + {errorText && ( + + +
+ {errorText} +
+
+
+ )} + {(actions || secondaryActions) && ( +
+
+ {actions &&
{actions}
} + {secondaryActions &&
{secondaryActions}
}
- - - )} - {(actions || secondaryActions) && ( -
-
- {actions &&
{actions}
} - {secondaryActions &&
{secondaryActions}
}
-
- )} - {errorText && ( - - )} -
+ )} + {errorText && ( + + )} +
+
); } diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 4a837ad86e..f16006e62f 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -4,6 +4,12 @@ /** Auto-generated argument types based on "en" i18n strings. Do not edit manually. */ export interface I18nFormatArgTypes { + "error-boundary": { + "i18nStrings.imageAltText"?: never; + "i18nStrings.messageHeader"?: never; + "i18nStrings.messageDescription"?: never; + "i18nStrings.messageFeedback"?: never; + }, "[charts]": { "loadingText": never; "errorText": never; diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index bfb588f6b7..6554e11b03 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -1,4 +1,10 @@ { + "error-boundary": { + "i18nStrings.imageAltText": "Application error", + "i18nStrings.messageHeader": "There was an unexpected issue", + "i18nStrings.messageDescription": "An unexpected issue led to a crash. [Refresh](refresh) the page to try again.", + "i18nStrings.messageFeedback": "If the issue persists, please provide us feedback [here](feedback)." + }, "[charts]": { "loadingText": "Loading chart", "errorText": "The data couldn't be fetched. Try again later.", @@ -454,4 +460,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } -} \ No newline at end of file +} diff --git a/src/modal/internal.tsx b/src/modal/internal.tsx index 16a04ed58d..203a152a91 100644 --- a/src/modal/internal.tsx +++ b/src/modal/internal.tsx @@ -7,7 +7,9 @@ import { useContainerQuery } from '@cloudscape-design/component-toolkit'; import { Portal, useMergeRefs, useUniqueId } from '@cloudscape-design/component-toolkit/internal'; import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; +import InternalBox from '../box/internal'; import { InternalButton } from '../button/internal'; +import InternalErrorBoundary from '../error-boundary/internal'; import InternalHeader from '../header/internal'; import { useInternalI18n } from '../i18n/context'; import { PerformanceMetrics } from '../internal/analytics'; @@ -283,21 +285,25 @@ function PortaledModal({
-
{content}} > - {children} -
-
- {footer && ( - -
- {footer} -
-
- )} +
+ {children} +
+
+ {footer && ( + +
+ {footer} +
+
+ )} +
diff --git a/src/popover/body.tsx b/src/popover/body.tsx index ee5ba52b7c..417b07b833 100644 --- a/src/popover/body.tsx +++ b/src/popover/body.tsx @@ -8,6 +8,7 @@ import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-tool import { ButtonProps } from '../button/interfaces'; import { InternalButton } from '../button/internal'; +import InternalErrorBoundary from '../error-boundary/internal'; import { useInternalI18n } from '../i18n/context'; import FocusLock from '../internal/components/focus-lock'; import { KeyCode } from '../internal/keycode'; @@ -102,22 +103,24 @@ export default function PopoverBody({ onKeyDown={onKeyDown} {...dialogProps} > - - {header && ( -
- {dismissButton} -
-

{header}

+ + + {header && ( +
+ {dismissButton} +
+

{header}

+
+
+ )} +
+ {!header && dismissButton} +
+ {children}
- )} -
- {!header && dismissButton} -
- {children} -
-
-
+ +
); } diff --git a/src/split-panel/implementation.tsx b/src/split-panel/implementation.tsx index 5a833dd6b2..352ee03528 100644 --- a/src/split-panel/implementation.tsx +++ b/src/split-panel/implementation.tsx @@ -10,6 +10,7 @@ import { SizeControlProps } from '../app-layout/utils/interfaces'; import { useKeyboardEvents } from '../app-layout/utils/use-keyboard-events'; import { usePointerEvents } from '../app-layout/utils/use-pointer-events'; import { InternalButton } from '../button/internal'; +import InternalErrorBoundary from '../error-boundary/internal'; import { getBaseProps } from '../internal/base-component'; import PanelResizeHandle from '../internal/components/panel-resize-handle'; import { useSplitPanelContext } from '../internal/context/split-panel-context'; @@ -260,7 +261,7 @@ export function SplitPanelImplementation({ ariaLabel={ariaLabel} closeBehavior={closeBehavior} > - {children} + {children} )} @@ -280,7 +281,7 @@ export function SplitPanelImplementation({ closeBehavior={closeBehavior} hasCustomElements={hasCustomElements} > - {children} + {children} )} {isPreferencesOpen && (