From d55704933db90a48fbf69e96d884379cb8d59132 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 19 Feb 2026 11:19:06 +0100 Subject: [PATCH 1/3] feat: AppLayout error boundaries --- package-lock.json | 1 + .../app-layout/with-error-boundaries.page.tsx | 334 ++++++++++++++++++ .../drawer/global-bottom-drawer.tsx | 29 +- .../drawer/global-drawers.tsx | 23 +- .../visual-refresh-toolbar/drawer/styles.scss | 40 +++ .../toolbar/drawer-triggers.tsx | 322 +++++++++-------- .../visual-refresh-toolbar/toolbar/index.tsx | 119 ++++--- .../widget-areas/after-main-slot.tsx | 21 +- .../widget-areas/before-main-slot.tsx | 61 ++-- 9 files changed, 725 insertions(+), 225 deletions(-) create mode 100644 pages/app-layout/with-error-boundaries.page.tsx diff --git a/package-lock.json b/package-lock.json index d663110273..66823ea2d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@cloudscape-design/components", "version": "3.0.0", + "hasInstallScript": true, "dependencies": { "@cloudscape-design/collection-hooks": "^1.0.0", "@cloudscape-design/component-toolkit": "^1.0.0-beta", diff --git a/pages/app-layout/with-error-boundaries.page.tsx b/pages/app-layout/with-error-boundaries.page.tsx new file mode 100644 index 0000000000..cad71fcd65 --- /dev/null +++ b/pages/app-layout/with-error-boundaries.page.tsx @@ -0,0 +1,334 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useContext, useRef, useState } from 'react'; + +import { AppLayout, Button, Container, ContentLayout, Header, HelpPanel, SpaceBetween, SplitPanel } from '~components'; +import { AppLayoutProps } from '~components/app-layout'; +import BreadcrumbGroup from '~components/breadcrumb-group'; +import ErrorBoundary from '~components/error-boundary'; +import I18nProvider from '~components/i18n'; +import messages from '~components/i18n/messages/all.en'; +import awsuiPlugins from '~components/internal/plugins'; +import { registerBottomDrawer, registerLeftDrawer } from '~components/internal/plugins/widget'; +import { mount, unmount } from '~mount'; + +import './utils/external-widget'; +import AppContext, { AppContextType } from '../app/app-context'; +import { CustomDrawerContent } from './utils/content-blocks'; +import { drawerLabels } from './utils/drawers'; +import appLayoutLabels from './utils/labels'; +import { splitPaneli18nStrings } from './utils/strings'; + +type DemoContext = React.Context< + AppContextType<{ + hasTools: boolean | undefined; + hasDrawers: boolean | undefined; + splitPanelPosition: AppLayoutProps.SplitPanelPreferences['position']; + }> +>; + +export default function WithErrorBoundariesPage() { + const [activeDrawerId, setActiveDrawerId] = useState(null); + const { urlParams, setUrlParams } = useContext(AppContext as DemoContext); + const [isToolsOpen, setIsToolsOpen] = useState(false); + const [isBrokenNavigation, setIsBrokenNavigation] = useState(false); + const appLayoutRef = useRef(null); + const [breadcrumbsItems, setBreadcrumbsItems] = useState([ + { text: 'Home', href: '#' }, + { text: 'Service', href: '#' }, + ]); + const drawersProps: Pick | null = { + activeDrawerId: activeDrawerId, + drawers: [ + { + ariaLabels: { + closeButton: 'ProHelp close button', + drawerName: 'ProHelp drawer content', + triggerButton: 'ProHelp trigger button', + resizeHandle: 'ProHelp resize handle', + }, + content: , + id: 'pro-help', + trigger: { + iconName: 'contact', + }, + }, + ], + onDrawerChange: event => { + setActiveDrawerId(event.detail.activeDrawerId); + }, + }; + + return ( + + +
+ +
Error boundaries in app layout slots
+ + } + > + + Toolbar}> + + + + + + Panels}> + + + + + + + + +
+
+ + } + splitPanel={ + + This is the Split Panel! + + } + splitPanelPreferences={{ + position: urlParams.splitPanelPosition, + }} + onSplitPanelPreferencesChange={event => { + const { position } = event.detail; + setUrlParams({ splitPanelPosition: position === 'side' ? position : undefined }); + }} + onToolsChange={event => { + setIsToolsOpen(event.detail.open); + }} + tools={} + toolsOpen={isToolsOpen} + navigation={isBrokenNavigation ? ({} as any) :
navigation
} + {...drawersProps} + /> +
+ ); +} + +function Info({ helpPathSlug }: { helpPathSlug: string }) { + return Info}>Here is some info for you: {helpPathSlug}; +} diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-bottom-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-bottom-drawer.tsx index 7fdabd08d2..dba7b3298f 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-bottom-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-bottom-drawer.tsx @@ -6,6 +6,7 @@ import clsx from 'clsx'; import { InternalItemOrGroup } from '../../../button-group/interfaces'; import ButtonGroup from '../../../button-group/internal'; +import { InternalErrorBoundary } from '../../../error-boundary/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; import customCssProps from '../../../internal/generated/custom-css-properties'; import { usePrevious } from '../../../internal/hooks/use-previous'; @@ -32,17 +33,25 @@ export function AppLayoutBottomDrawerWrapper({ widgetizedState }: { widgetizedSt <> {bottomDrawers.map(drawer => { return ( - + className={styles['drawer-error-boundary']} + onError={error => console.log('Error boundary for the local drawer: ', error)} + suppressNested={false} + suppressible={true} + > + + ); })} diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawers.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawers.tsx index 4afe0eef34..97dad64e15 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawers.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawers.tsx @@ -2,9 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useRef } from 'react'; +import { InternalErrorBoundary } from '../../../error-boundary/internal'; import { AppLayoutInternals } from '../interfaces'; import AppLayoutGlobalDrawer from './global-drawer'; +import styles from './styles.css.js'; + interface AppLayoutGlobalDrawersImplementationProps { appLayoutInternals: AppLayoutInternals; } @@ -30,12 +33,22 @@ export function AppLayoutGlobalDrawersImplementation({ .map(drawer => { openDrawersHistory.current.add(drawer.id); return ( - + onError={error => console.log('Error boundary for the trigger button: ', error)} + suppressNested={false} + suppressible={true} + > + + ); })} diff --git a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss index d1879fa43d..e7a2148d45 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss @@ -25,6 +25,46 @@ $global-drawer-expanded-mode-motion: #{awsui.$motion-duration-refresh-only-slow} $drawer-resize-handle-size: awsui.$space-m; $ai-drawer-heider-height: 42px; +.ai-drawer-error-boundary { + position: sticky; + z-index: constants.$drawer-z-index; + background-color: awsui.$color-background-container-content; + display: grid; + inline-size: var(#{custom-props.$drawerSize}); + + block-size: 100%; + overflow: hidden; + /* stylelint-disable-next-line plugin/no-unsupported-browser-features */ + overscroll-behavior-y: contain; + pointer-events: auto; + word-wrap: break-word; + border-start-end-radius: awsui.$space-xxs; + min-inline-size: 300px; + + @include mobile-only { + margin-block-start: 42px; + inline-size: 100%; + } +} + +.drawer-error-boundary { + @include mobile-only { + position: sticky; + z-index: constants.$drawer-z-index; + background-color: awsui.$color-background-container-content; + display: grid; + inline-size: var(#{custom-props.$drawerSize}); + margin-block-start: 42px; + + block-size: 100%; + overflow: hidden; + /* stylelint-disable-next-line plugin/no-unsupported-browser-features */ + overscroll-behavior-y: contain; + pointer-events: auto; + word-wrap: break-word; + } +} + .drawer { position: sticky; z-index: constants.$drawer-z-index; diff --git a/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx b/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx index 4df4cf7aab..6dd347dd70 100644 --- a/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx +++ b/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx @@ -5,7 +5,11 @@ import clsx from 'clsx'; import { useContainerQuery } from '@cloudscape-design/component-toolkit'; +import Box from '../../../box/internal'; +import { InternalErrorBoundary } from '../../../error-boundary/internal'; import { useMobile } from '../../../internal/hooks/use-mobile'; +import Popover from '../../../popover/internal'; +import StatusIndicator from '../../../status-indicator/internal'; import { splitItems } from '../../drawer/drawers-helpers'; import OverflowMenu from '../../drawer/overflow-menu'; import { AppLayoutProps, AppLayoutPropsWithDefaults } from '../../interfaces'; @@ -52,6 +56,35 @@ interface DrawerTriggersProps { disabled: boolean; } +const TriggerButtonErrorBoundary: React.FC<{ id: string }> = ({ id, children }) => { + return ( + console.log('Error boundary for the trigger button: ', error)} + suppressNested={false} + suppressible={true} + renderFallback={props => { + return ( + + {props.description} + {props.action} + + } + > + + + ); + }} + > + {children} + + ); +}; + export function DrawerTriggers({ ariaLabels, activeDrawerId, @@ -150,67 +183,71 @@ export function DrawerTriggers({ > {splitPanelToggleProps && ( <> - { - exitExpandedMode(); - if (!!expandedDrawerId && splitPanelToggleProps.active) { - return; - } - onSplitPanelToggle?.(); - }} - selected={!expandedDrawerId && splitPanelToggleProps.active} - ref={splitPanelResolvedPosition === 'side' ? splitPanelFocusRef : undefined} - hasTooltip={true} - isMobile={isMobile} - isForSplitPanel={true} - disabled={disabled} - /> - {hasMultipleTriggers ?
: null} + + { + exitExpandedMode(); + if (!!expandedDrawerId && splitPanelToggleProps.active) { + return; + } + onSplitPanelToggle?.(); + }} + selected={!expandedDrawerId && splitPanelToggleProps.active} + ref={splitPanelResolvedPosition === 'side' ? splitPanelFocusRef : undefined} + hasTooltip={true} + isMobile={isMobile} + isForSplitPanel={true} + disabled={disabled} + /> + {hasMultipleTriggers ?
: null} +
)} {visibleItems.slice(0, globalDrawersStartIndex).map(item => { const isForPreviousActiveDrawer = previousActiveLocalDrawerId?.current === item.id; const selected = !expandedDrawerId && item.id === activeDrawerId; return ( - { - exitExpandedMode(); - if (!!expandedDrawerId && activeDrawerId === item.id) { - return; - } - onActiveDrawerChange?.(activeDrawerId !== item.id ? item.id : null, { initiatedByUserAction: true }); - }} - ref={item.id === previousActiveLocalDrawerId.current ? drawersFocusRef : undefined} - selected={selected} - badge={item.badge} - testId={`awsui-app-layout-trigger-${item.id}`} - hasTooltip={true} - hasOpenDrawer={hasOpenDrawer} - tooltipText={item.ariaLabels?.drawerName} - isForPreviousActiveDrawer={isForPreviousActiveDrawer} - isMobile={isMobile} - disabled={disabled} - /> + + { + exitExpandedMode(); + if (!!expandedDrawerId && activeDrawerId === item.id) { + return; + } + onActiveDrawerChange?.(activeDrawerId !== item.id ? item.id : null, { initiatedByUserAction: true }); + }} + ref={item.id === previousActiveLocalDrawerId.current ? drawersFocusRef : undefined} + selected={selected} + badge={item.badge} + testId={`awsui-app-layout-trigger-${item.id}`} + hasTooltip={true} + hasOpenDrawer={hasOpenDrawer} + tooltipText={item.ariaLabels?.drawerName} + isForPreviousActiveDrawer={isForPreviousActiveDrawer} + isMobile={isMobile} + disabled={disabled} + /> + ); })} {globalDrawersStartIndex > 0 && visibleItems.length > globalDrawersStartIndex && ( @@ -227,98 +264,103 @@ export function DrawerTriggers({ } return ( - { - exitExpandedMode(); - if (!!expandedDrawerId && item.id !== expandedDrawerId && activeGlobalDrawersIds.includes(item.id)) { - return; + + { + exitExpandedMode(); + if (!!expandedDrawerId && item.id !== expandedDrawerId && activeGlobalDrawersIds.includes(item.id)) { + return; + } + if (isBottom) { + onActiveGlobalBottomDrawerChange?.(selected ? null : item.id, { initiatedByUserAction: true }); + return; + } + onActiveGlobalDrawersChange?.(item.id, { initiatedByUserAction: true }); + }} + ref={isBottom ? bottomDrawersFocusRef : globalDrawersFocusControl?.refs[item.id]?.toggle} + selected={selected} + badge={item.badge} + testId={`awsui-app-layout-trigger-${item.id}`} + hasTooltip={true} + hasOpenDrawer={hasOpenDrawer} + tooltipText={item.ariaLabels?.drawerName} + isForPreviousActiveDrawer={isForPreviousActiveDrawer} + isMobile={isMobile} + disabled={disabled} + /> + + ); + })} + {overflowItems.length > 0 && ( + + { + const isBottom = item?.position === 'bottom'; + let active = + activeGlobalDrawersIds.includes(item.id) && (!expandedDrawerId || item.id === expandedDrawerId); + if (isBottom) { + active = + item.id === activeGlobalBottomDrawerId && (!expandedDrawerId || item.id === expandedDrawerId); } + return { + ...item, + active, + }; + })} + ariaLabel={overflowMenuHasBadge ? ariaLabels?.drawersOverflowWithBadge : ariaLabels?.drawersOverflow} + customTriggerBuilder={({ onClick, triggerRef, ariaLabel, ariaExpanded, testUtilsClass }) => { + return ( + + ); + }} + onItemClick={event => { + const id = event.detail.id; + exitExpandedMode(); + const item = overflowItems.find(item => item.id === id); + const isBottom = item?.position === 'bottom'; if (isBottom) { + const selected = + item.id === activeGlobalBottomDrawerId && (!expandedDrawerId || item.id === expandedDrawerId); onActiveGlobalBottomDrawerChange?.(selected ? null : item.id, { initiatedByUserAction: true }); return; } - onActiveGlobalDrawersChange?.(item.id, { initiatedByUserAction: true }); + if (globalDrawers.find(drawer => drawer.id === id)) { + if (!!expandedDrawerId && id !== expandedDrawerId && activeGlobalDrawersIds.includes(id)) { + return; + } + onActiveGlobalDrawersChange?.(id, { initiatedByUserAction: true }); + } else { + onActiveDrawerChange?.(event.detail.id, { initiatedByUserAction: true }); + } }} - ref={isBottom ? bottomDrawersFocusRef : globalDrawersFocusControl?.refs[item.id]?.toggle} - selected={selected} - badge={item.badge} - testId={`awsui-app-layout-trigger-${item.id}`} - hasTooltip={true} - hasOpenDrawer={hasOpenDrawer} - tooltipText={item.ariaLabels?.drawerName} - isForPreviousActiveDrawer={isForPreviousActiveDrawer} - isMobile={isMobile} - disabled={disabled} + globalDrawersStartIndex={globalDrawersStartIndex - indexOfOverflowItem} /> - ); - })} - {overflowItems.length > 0 && ( - { - const isBottom = item?.position === 'bottom'; - let active = - activeGlobalDrawersIds.includes(item.id) && (!expandedDrawerId || item.id === expandedDrawerId); - if (isBottom) { - active = item.id === activeGlobalBottomDrawerId && (!expandedDrawerId || item.id === expandedDrawerId); - } - return { - ...item, - active, - }; - })} - ariaLabel={overflowMenuHasBadge ? ariaLabels?.drawersOverflowWithBadge : ariaLabels?.drawersOverflow} - customTriggerBuilder={({ onClick, triggerRef, ariaLabel, ariaExpanded, testUtilsClass }) => { - return ( - - ); - }} - onItemClick={event => { - const id = event.detail.id; - exitExpandedMode(); - const item = overflowItems.find(item => item.id === id); - const isBottom = item?.position === 'bottom'; - if (isBottom) { - const selected = - item.id === activeGlobalBottomDrawerId && (!expandedDrawerId || item.id === expandedDrawerId); - onActiveGlobalBottomDrawerChange?.(selected ? null : item.id, { initiatedByUserAction: true }); - return; - } - if (globalDrawers.find(drawer => drawer.id === id)) { - if (!!expandedDrawerId && id !== expandedDrawerId && activeGlobalDrawersIds.includes(id)) { - return; - } - onActiveGlobalDrawersChange?.(id, { initiatedByUserAction: true }); - } else { - onActiveDrawerChange?.(event.detail.id, { initiatedByUserAction: true }); - } - }} - globalDrawersStartIndex={globalDrawersStartIndex - indexOfOverflowItem} - /> + )} diff --git a/src/app-layout/visual-refresh-toolbar/toolbar/index.tsx b/src/app-layout/visual-refresh-toolbar/toolbar/index.tsx index a0b6414524..71625a4ab9 100644 --- a/src/app-layout/visual-refresh-toolbar/toolbar/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/toolbar/index.tsx @@ -6,7 +6,11 @@ import clsx from 'clsx'; import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal'; +import Box from '../../../box/internal'; +import { InternalErrorBoundary } from '../../../error-boundary/internal'; import { createWidgetizedComponent } from '../../../internal/widgets'; +import Popover from '../../../popover/internal'; +import StatusIndicator from '../../../status-indicator/internal'; import { AppLayoutProps } from '../../interfaces'; import { OnChangeParams } from '../../utils/use-drawers'; import { Focusable, FocusControlMultipleStates } from '../../utils/use-focus-control'; @@ -68,6 +72,35 @@ export interface AppLayoutToolbarImplementationProps { toolbarProps: ToolbarProps; } +export const ToolbarSectionErrorBoundary: React.FC<{ id: string }> = ({ id, children }) => { + return ( + console.log('Error boundary for the trigger button: ', error)} + suppressNested={false} + suppressible={true} + renderFallback={props => { + return ( + + {props.description} + {props.action} + + } + > + + + ); + }} + > + {children} + + ); +}; + export function AppLayoutToolbarImplementation({ appLayoutInternals, // the value could be undefined if this component is loaded as a widget by a different app layout version @@ -171,26 +204,28 @@ export function AppLayoutToolbarImplementation({ opacity: ['entering', 'exiting'].includes(state) ? 0 : 1, }} > - { - if (setExpandedDrawerId) { - setExpandedDrawerId(null); - } - onActiveAiDrawerChange?.(aiDrawer?.id ?? null, { initiatedByUserAction: true }); - }} - ref={aiDrawerFocusRef} - selected={!drawerExpandedMode && !!activeAiDrawerId} - disabled={anyPanelOpenInMobile} - variant={aiDrawer?.trigger?.customIcon ? 'custom' : 'circle'} - testId={`awsui-app-layout-trigger-${aiDrawer?.id}`} - isForPreviousActiveDrawer={true} - /> + + { + if (setExpandedDrawerId) { + setExpandedDrawerId(null); + } + onActiveAiDrawerChange?.(aiDrawer?.id ?? null, { initiatedByUserAction: true }); + }} + ref={aiDrawerFocusRef} + selected={!drawerExpandedMode && !!activeAiDrawerId} + disabled={anyPanelOpenInMobile} + variant={aiDrawer?.trigger?.customIcon ? 'custom' : 'circle'} + testId={`awsui-app-layout-trigger-${aiDrawer?.id}`} + isForPreviousActiveDrawer={true} + /> + )} @@ -229,27 +264,29 @@ export function AppLayoutToolbarImplementation({ bottomDrawers?.length || (hasSplitPanel && splitPanelToggleProps?.displayed)) && (
- !!item.trigger) ?? []} - drawersFocusRef={drawersFocusRef} - onActiveDrawerChange={onActiveDrawerChange} - splitPanelToggleProps={splitPanelToggleProps?.displayed ? splitPanelToggleProps : undefined} - splitPanelFocusRef={splitPanelFocusRef} - onSplitPanelToggle={onSplitPanelToggle} - disabled={anyPanelOpenInMobile} - globalDrawersFocusControl={globalDrawersFocusControl} - bottomDrawersFocusRef={bottomDrawersFocusRef} - globalDrawers={globalDrawers?.filter(item => !!item.trigger) ?? []} - activeGlobalDrawersIds={activeGlobalDrawersIds ?? []} - onActiveGlobalDrawersChange={onActiveGlobalDrawersChange} - expandedDrawerId={expandedDrawerId} - setExpandedDrawerId={setExpandedDrawerId!} - bottomDrawers={bottomDrawers} - onActiveGlobalBottomDrawerChange={onActiveGlobalBottomDrawerChange} - activeGlobalBottomDrawerId={activeGlobalBottomDrawerId} - /> + + !!item.trigger) ?? []} + drawersFocusRef={drawersFocusRef} + onActiveDrawerChange={onActiveDrawerChange} + splitPanelToggleProps={splitPanelToggleProps?.displayed ? splitPanelToggleProps : undefined} + splitPanelFocusRef={splitPanelFocusRef} + onSplitPanelToggle={onSplitPanelToggle} + disabled={anyPanelOpenInMobile} + globalDrawersFocusControl={globalDrawersFocusControl} + bottomDrawersFocusRef={bottomDrawersFocusRef} + globalDrawers={globalDrawers?.filter(item => !!item.trigger) ?? []} + activeGlobalDrawersIds={activeGlobalDrawersIds ?? []} + onActiveGlobalDrawersChange={onActiveGlobalDrawersChange} + expandedDrawerId={expandedDrawerId} + setExpandedDrawerId={setExpandedDrawerId!} + bottomDrawers={bottomDrawers} + onActiveGlobalBottomDrawerChange={onActiveGlobalBottomDrawerChange} + activeGlobalBottomDrawerId={activeGlobalBottomDrawerId} + /> +
)} diff --git a/src/app-layout/visual-refresh-toolbar/widget-areas/after-main-slot.tsx b/src/app-layout/visual-refresh-toolbar/widget-areas/after-main-slot.tsx index 85c2a1d406..9aa1c584c1 100644 --- a/src/app-layout/visual-refresh-toolbar/widget-areas/after-main-slot.tsx +++ b/src/app-layout/visual-refresh-toolbar/widget-areas/after-main-slot.tsx @@ -3,6 +3,7 @@ import React from 'react'; import clsx from 'clsx'; +import { InternalErrorBoundary } from '../../../error-boundary/internal'; import { createWidgetizedComponent } from '../../../internal/widgets'; import { ActiveDrawersContext } from '../../utils/visibility-context'; import { @@ -15,6 +16,7 @@ import { AppLayoutSplitPanelDrawerSideImplementation as AppLayoutSplitPanelSide import { isWidgetReady } from '../state/invariants'; import sharedStyles from '../../resize/styles.css.js'; +import drawerStyles from '../drawer/styles.css.js'; import styles from '../skeleton/styles.css.js'; export const AfterMainSlotImplementation = ({ appLayoutState, appLayoutProps }: SkeletonPartProps) => { @@ -73,12 +75,19 @@ export const AfterMainSlotImplementation = ({ appLayoutState, appLayoutProps }: drawerExpandedMode && styles.hidden )} > - {drawers && drawers.length > 0 && ( - - )} + console.log('Error boundary for the local drawer: ', error)} + suppressNested={false} + suppressible={true} + > + {drawers && drawers.length > 0 && ( + + )} +
diff --git a/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx b/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx index b8325b7fcf..3e2183205f 100644 --- a/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx +++ b/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx @@ -3,6 +3,7 @@ import React, { useRef } from 'react'; import clsx from 'clsx'; +import { InternalErrorBoundary } from '../../../error-boundary/internal'; import { createWidgetizedComponent } from '../../../internal/widgets'; import { ActiveDrawersContext } from '../../utils/visibility-context'; import { AppLayoutGlobalAiDrawerImplementation } from '../drawer/global-ai-drawer'; @@ -13,6 +14,7 @@ import { isWidgetReady } from '../state/invariants'; import { AppLayoutToolbarImplementation as AppLayoutToolbar } from '../toolbar'; import sharedStyles from '../../resize/styles.css.js'; +import drawerStyles from '../drawer/styles.css.js'; import styles from '../skeleton/styles.css.js'; export const BeforeMainSlotImplementation = ({ toolbarProps, appLayoutState, appLayoutProps }: SkeletonPartProps) => { @@ -69,25 +71,32 @@ export const BeforeMainSlotImplementation = ({ toolbarProps, appLayoutState, app {(!!activeAiDrawerId || (aiDrawer?.preserveInactiveContent && wasAiDrawerOpenRef.current)) && ( <> {(wasAiDrawerOpenRef.current = true)} - onActiveAiDrawerResize(size), - expandedDrawerId, - setExpandedDrawerId, - }} - /> + console.log('Error boundary for the local drawer: ', error)} + suppressNested={false} + suppressible={true} + > + onActiveAiDrawerResize(size), + expandedDrawerId, + setExpandedDrawerId, + }} + /> + )} @@ -103,10 +112,16 @@ export const BeforeMainSlotImplementation = ({ toolbarProps, appLayoutState, app (drawerExpandedMode || drawerExpandedModeInChildLayout) && styles.hidden )} > - + console.log('Error boundary for the nav panel: ', error)} + suppressNested={false} + suppressible={true} + > + +
)} From 212a503f5d35884a346d326208c5dfa22ed996df Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Fri, 20 Feb 2026 13:56:56 +0100 Subject: [PATCH 2/3] chore: Adjust styles, make left drawer error boundaries more granular --- .../app-layout/with-error-boundaries.page.tsx | 38 ++++- .../drawer/global-ai-drawer.tsx | 58 ++++--- .../drawer/global-bottom-drawer.tsx | 159 +++++++++--------- .../drawer/global-drawer.tsx | 63 ++++--- .../drawer/global-drawers.tsx | 23 +-- .../drawer/local-drawer.tsx | 17 +- .../toolbar/drawer-triggers.tsx | 3 +- .../visual-refresh-toolbar/toolbar/index.tsx | 34 +++- .../toolbar/styles.scss | 7 + .../widget-areas/after-main-slot.tsx | 21 +-- .../widget-areas/before-main-slot.tsx | 46 +++-- 11 files changed, 278 insertions(+), 191 deletions(-) diff --git a/pages/app-layout/with-error-boundaries.page.tsx b/pages/app-layout/with-error-boundaries.page.tsx index cad71fcd65..03ff82e45c 100644 --- a/pages/app-layout/with-error-boundaries.page.tsx +++ b/pages/app-layout/with-error-boundaries.page.tsx @@ -203,7 +203,43 @@ export default function WithErrorBoundariesPage() { }); }} > - Break left drawer on mount + Break left drawer content on mount + + - - + + - + - - Panels}> - + + Panels}> + - + - - + + - + - + - - - - - - - } - splitPanel={ - - This is the Split Panel! - - } - splitPanelPreferences={{ - position: urlParams.splitPanelPosition, - }} - onSplitPanelPreferencesChange={event => { - const { position } = event.detail; - setUrlParams({ splitPanelPosition: position === 'side' ? position : undefined }); - }} - onToolsChange={event => { - setIsToolsOpen(event.detail.open); - }} - tools={} - toolsOpen={isToolsOpen} - navigation={isBrokenNavigation ? ({} as any) :
navigation
} - {...drawersProps} - /> + mountContent: () => { + throw new Error('Mount error in drawer content'); + }, + unmountContent: container => unmount(container), + }); + }} + > + Break bottom drawer on mount + + + + + + + + } + splitPanel={ + + This is the Split Panel! + + } + splitPanelPreferences={{ + position: urlParams.splitPanelPosition, + }} + onSplitPanelPreferencesChange={event => { + const { position } = event.detail; + setUrlParams({ splitPanelPosition: position === 'side' ? position : undefined }); + }} + onToolsChange={event => { + setIsToolsOpen(event.detail.open); + }} + tools={} + toolsOpen={isToolsOpen} + navigation={isBrokenNavigation ? ({} as any) :
navigation
} + {...drawersProps} + /> + ); } 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 112c458c37..26ba5b1a2f 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx @@ -5,7 +5,7 @@ import { Transition } from 'react-transition-group'; import clsx from 'clsx'; import { InternalButton } from '../../../button/internal'; -import { InternalErrorBoundary } from '../../../error-boundary/internal'; +import { BuiltInErrorBoundary } from '../../../error-boundary/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; import customCssProps from '../../../internal/generated/custom-css-properties'; import { getLimitedValue } from '../../../split-panel/utils/size-utils'; @@ -144,23 +144,11 @@ export function AppLayoutDrawerImplementation({ )} style={{ blockSize: drawerHeight }} > - console.log('Error boundary for the local drawer: ', error)} - suppressNested={false} - suppressible={true} - > - {toolsContent} - + {toolsContent} {activeDrawerId !== TOOLS_DRAWER_ID && (
- console.log('Error boundary for the local drawer: ', error)} - suppressNested={false} - suppressible={true} - > - {activeDrawer?.content} - + {activeDrawer?.content}
)}