Skip to content

Commit 2826547

Browse files
committed
poc
1 parent a0595cc commit 2826547

File tree

14 files changed

+550
-88
lines changed

14 files changed

+550
-88
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
{
168168
"path": "lib/components/internal/widget-exports.js",
169169
"brotli": false,
170-
"limit": "810 kB",
170+
"limit": "840 kB",
171171
"ignore": "react-dom"
172172
}
173173
],

pages/app/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function isAppLayoutPage(pageId?: string) {
4242
'prompt-input/simple',
4343
'funnel-analytics/static-single-page-flow',
4444
'funnel-analytics/static-multi-page-flow',
45+
'error-boundaries',
4546
];
4647
return pageId !== undefined && appLayoutPages.some(match => pageId.includes(match));
4748
}

pages/error-boundaries/demo.page.tsx

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { useContext, useState } from 'react';
5+
6+
import {
7+
AppLayout,
8+
Box,
9+
Button,
10+
Checkbox,
11+
Container,
12+
Drawer,
13+
ExpandableSection,
14+
Header,
15+
Modal,
16+
Popover,
17+
SpaceBetween,
18+
SplitPanel,
19+
Table,
20+
} from '~components';
21+
import { ErrorBoundariesProvider } from '~components/error-boundary/context';
22+
import I18nProvider from '~components/i18n';
23+
import messages from '~components/i18n/messages/all.en';
24+
import {
25+
colorBackgroundStatusError,
26+
colorBorderStatusError,
27+
colorTextInteractiveInvertedHover,
28+
colorTextStatusError,
29+
} from '~design-tokens';
30+
31+
import AppContext, { AppContextType } from '../app/app-context';
32+
import ScreenshotArea from '../utils/screenshot-area';
33+
34+
type PageContext = React.Context<AppContextType<{ errorBoundariesActive: boolean }>>;
35+
36+
export default function () {
37+
const {
38+
urlParams: { errorBoundariesActive = true },
39+
setUrlParams,
40+
} = useContext(AppContext as PageContext);
41+
const [splitPanelOpen, setSplitPanelOpen] = useState(true);
42+
const [activeDrawerId, setActiveDrawerId] = useState<null | string>('d1');
43+
const [modalOpen, setModalOpen] = useState(false);
44+
return (
45+
<ScreenshotArea gutters={false}>
46+
<I18nProvider messages={[messages]} locale="en">
47+
<ErrorBoundariesProvider active={errorBoundariesActive} value={{ feedbackLink: '/#' }}>
48+
<AppLayout
49+
navigationHide={true}
50+
activeDrawerId={activeDrawerId}
51+
onDrawerChange={({ detail }) => setActiveDrawerId(detail.activeDrawerId)}
52+
splitPanel={
53+
<SplitPanel header="Split panel">
54+
<BrokenButton />
55+
</SplitPanel>
56+
}
57+
splitPanelOpen={splitPanelOpen}
58+
onSplitPanelToggle={({ detail }) => setSplitPanelOpen(detail.open)}
59+
drawers={[
60+
{
61+
id: 'd1',
62+
content: (
63+
<Drawer header={<Header variant="h2">Drawer 1</Header>}>
64+
<BrokenButton />
65+
</Drawer>
66+
),
67+
trigger: { iconName: 'bug' },
68+
ariaLabels: { drawerName: 'Drawer 1', triggerButton: 'Open drawer 1', closeButton: 'Close drawer 1' },
69+
},
70+
{
71+
id: 'd2',
72+
content: (
73+
<Drawer header={<Header variant="h2">Drawer 2</Header>}>
74+
<BrokenButton />
75+
</Drawer>
76+
),
77+
trigger: { iconName: 'call' },
78+
ariaLabels: { drawerName: 'Drawer 2', triggerButton: 'Open drawer 2', closeButton: 'Close drawer 2' },
79+
},
80+
]}
81+
content={
82+
<Box>
83+
<h1>Error boundaries page</h1>
84+
<Box margin={{ bottom: 'm' }}>
85+
<Checkbox
86+
checked={errorBoundariesActive}
87+
onChange={({ detail }) => setUrlParams({ errorBoundariesActive: detail.checked })}
88+
>
89+
Error boundaries on
90+
</Checkbox>
91+
</Box>
92+
93+
<SpaceBetween size="m">
94+
<SpaceBetween size="m" direction="horizontal">
95+
<Box>
96+
<Button onClick={() => setModalOpen(true)}>Show modal</Button>
97+
<Modal visible={modalOpen} header="Modal" onDismiss={() => setModalOpen(false)}>
98+
<BrokenButton />
99+
</Modal>
100+
</Box>
101+
102+
<Popover header="Header" content={<BrokenButton />} triggerType="custom">
103+
<Button>Show popover</Button>
104+
</Popover>
105+
</SpaceBetween>
106+
107+
<Container header={<Header>Container 1</Header>}>
108+
<BrokenButton />
109+
</Container>
110+
111+
<Container header={<Header>Container 2</Header>}>
112+
<BrokenButton />
113+
</Container>
114+
115+
<ExpandableSection variant="container" headerText="Expandable section" defaultExpanded={true}>
116+
<BrokenButton />
117+
</ExpandableSection>
118+
119+
<Table
120+
header={<Header>Table</Header>}
121+
columnDefinitions={[
122+
{ header: 'Column 1', cell: item => `Content 1:${item}` },
123+
{ header: 'Column 2', cell: item => `Content 2:${item}` },
124+
{ header: 'Column 3', cell: item => `Content 3:${item}` },
125+
{ header: 'Actions', cell: () => <BrokenButton /> },
126+
]}
127+
items={[1, 2, 3]}
128+
></Table>
129+
</SpaceBetween>
130+
</Box>
131+
}
132+
/>
133+
</ErrorBoundariesProvider>
134+
</I18nProvider>
135+
</ScreenshotArea>
136+
);
137+
}
138+
139+
function BrokenButton() {
140+
const [errorState, setErrorState] = useState(false);
141+
const colorsDefault = [colorTextStatusError, colorBorderStatusError, undefined];
142+
const colorsHover = [colorTextStatusError, colorBorderStatusError, colorBackgroundStatusError];
143+
const colorsActive = [colorTextInteractiveInvertedHover, colorBorderStatusError, colorTextStatusError];
144+
return (
145+
<Button
146+
style={{
147+
root: {
148+
color: { default: colorsDefault[0], hover: colorsHover[0], active: colorsActive[0] },
149+
borderColor: { default: colorsDefault[1], hover: colorsHover[1], active: colorsActive[1] },
150+
background: { default: colorsDefault[2], hover: colorsHover[2], active: colorsActive[2] },
151+
},
152+
}}
153+
onClick={() => setErrorState(true)}
154+
>
155+
Broken button {errorState ? {} : ''}
156+
</Button>
157+
);
158+
}

src/container/internal.tsx

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3+
34
import React, { useRef } from 'react';
45
import clsx from 'clsx';
56

67
import { useMergeRefs, useUniqueId } from '@cloudscape-design/component-toolkit/internal';
78
import { getAnalyticsLabelAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
89

10+
import InternalBox from '../box/internal';
11+
import InternalErrorBoundary from '../error-boundary/internal';
912
import { useFunnelSubStep } from '../internal/analytics/hooks/use-funnel';
1013
import { getBaseProps } from '../internal/base-component';
1114
import { ContainerHeaderContextProvider } from '../internal/context/container-header';
@@ -145,60 +148,62 @@ export default function InternalContainer({
145148
ref={__subStepRef}
146149
className={clsx(styles['content-wrapper'], fitHeight && styles['content-wrapper-fit-height'])}
147150
>
148-
{header && (
149-
<ContainerHeaderContextProvider>
150-
<StickyHeaderContext.Provider value={{ isStuck, isStuckAtBottom }}>
151-
<div
152-
className={clsx(
153-
isRefresh && styles.refresh,
154-
styles.header,
155-
analyticsSelectors.header,
156-
styles[`header-variant-${variant}`],
157-
{
158-
[styles['header-sticky-disabled']]: __stickyHeader && !isSticky,
159-
[styles['header-sticky-enabled']]: isSticky,
160-
[styles['header-dynamic-height']]: hasDynamicHeight,
161-
[styles['header-stuck']]: isStuck,
162-
[styles['with-paddings']]: !disableHeaderPaddings,
163-
[styles['with-hidden-content']]: !children || __hiddenContent,
164-
[styles['header-with-media']]: hasMedia,
165-
[styles['header-full-page']]: __fullPage && isRefresh,
166-
}
167-
)}
168-
ref={headerMergedRef}
169-
style={{
170-
...stickyStyles.style,
171-
...getHeaderStyles(style),
172-
}}
173-
>
174-
{isStuck && !isMobile && isRefresh && __fullPage && <div className={styles['header-cover']}></div>}
175-
{header}
176-
</div>
177-
</StickyHeaderContext.Provider>
178-
</ContainerHeaderContextProvider>
179-
)}
180-
<div className={clsx(styles.content, fitHeight && styles['content-fit-height'])}>
181-
<div
182-
className={clsx(styles['content-inner'], testStyles['content-inner'], {
183-
[styles['with-paddings']]: !disableContentPaddings,
184-
[styles['with-header']]: !!header,
185-
})}
186-
style={getContentStyles(style)}
187-
>
188-
{children}
189-
</div>
190-
</div>
191-
{footer && (
192-
<div
193-
className={clsx(styles.footer, {
194-
[styles['with-divider']]: !__disableFooterDivider,
195-
[styles['with-paddings']]: !disableFooterPaddings,
196-
})}
197-
style={getFooterStyles(style)}
198-
>
199-
{footer}
151+
<InternalErrorBoundary wrapper={content => <InternalBox padding="m">{content}</InternalBox>}>
152+
{header && (
153+
<ContainerHeaderContextProvider>
154+
<StickyHeaderContext.Provider value={{ isStuck, isStuckAtBottom }}>
155+
<div
156+
className={clsx(
157+
isRefresh && styles.refresh,
158+
styles.header,
159+
analyticsSelectors.header,
160+
styles[`header-variant-${variant}`],
161+
{
162+
[styles['header-sticky-disabled']]: __stickyHeader && !isSticky,
163+
[styles['header-sticky-enabled']]: isSticky,
164+
[styles['header-dynamic-height']]: hasDynamicHeight,
165+
[styles['header-stuck']]: isStuck,
166+
[styles['with-paddings']]: !disableHeaderPaddings,
167+
[styles['with-hidden-content']]: !children || __hiddenContent,
168+
[styles['header-with-media']]: hasMedia,
169+
[styles['header-full-page']]: __fullPage && isRefresh,
170+
}
171+
)}
172+
ref={headerMergedRef}
173+
style={{
174+
...stickyStyles.style,
175+
...getHeaderStyles(style),
176+
}}
177+
>
178+
{isStuck && !isMobile && isRefresh && __fullPage && <div className={styles['header-cover']}></div>}
179+
{header}
180+
</div>
181+
</StickyHeaderContext.Provider>
182+
</ContainerHeaderContextProvider>
183+
)}
184+
<div className={clsx(styles.content, fitHeight && styles['content-fit-height'])}>
185+
<div
186+
className={clsx(styles['content-inner'], testStyles['content-inner'], {
187+
[styles['with-paddings']]: !disableContentPaddings,
188+
[styles['with-header']]: !!header,
189+
})}
190+
style={getContentStyles(style)}
191+
>
192+
{children}
193+
</div>
200194
</div>
201-
)}
195+
{footer && (
196+
<div
197+
className={clsx(styles.footer, {
198+
[styles['with-divider']]: !__disableFooterDivider,
199+
[styles['with-paddings']]: !disableFooterPaddings,
200+
})}
201+
style={getFooterStyles(style)}
202+
>
203+
{footer}
204+
</div>
205+
)}
206+
</InternalErrorBoundary>
202207
</div>
203208
</div>
204209
);

src/drawer/implementation.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import clsx from 'clsx';
55

66
import { useRuntimeDrawerContext } from '../app-layout/runtime-drawer/use-runtime-drawer-context';
77
import { useAppLayoutToolbarDesignEnabled } from '../app-layout/utils/feature-flags';
8+
import InternalErrorBoundary from '../error-boundary/internal';
89
import { useInternalI18n } from '../i18n/context';
910
import { getBaseProps } from '../internal/base-component';
1011
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
@@ -36,7 +37,7 @@ export function DrawerImplementation({
3637
};
3738

3839
const runtimeDrawerContext = useRuntimeDrawerContext({ rootRef: __internalRootRef as RefObject<HTMLElement> });
39-
const hasAdditioalDrawerAction = !!runtimeDrawerContext?.isExpandable;
40+
const hasAdditionalDrawerAction = !!runtimeDrawerContext?.isExpandable;
4041

4142
return loading ? (
4243
<div
@@ -57,7 +58,7 @@ export function DrawerImplementation({
5758
className={clsx(
5859
styles.header,
5960
runtimeDrawerContext && styles['with-runtime-context'],
60-
hasAdditioalDrawerAction && styles['with-additional-action']
61+
hasAdditionalDrawerAction && styles['with-additional-action']
6162
)}
6263
>
6364
{header}
@@ -70,7 +71,7 @@ export function DrawerImplementation({
7071
!disableContentPaddings && styles['content-with-paddings']
7172
)}
7273
>
73-
{children}
74+
<InternalErrorBoundary>{children}</InternalErrorBoundary>
7475
</div>
7576
</div>
7677
);

src/error-boundary/context.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { createContext } from 'react';
5+
6+
import { ErrorBoundaryProps } from './interfaces';
7+
8+
interface ErrorBoundariesContextValue {
9+
customMessage?: ErrorBoundaryProps.CustomMessage;
10+
feedbackLink?: string;
11+
i18nStrings?: ErrorBoundaryProps.I18nStrings;
12+
}
13+
14+
export const ErrorBoundariesContext = createContext<ErrorBoundariesContextValue & { errorBoundariesActive: boolean }>({
15+
errorBoundariesActive: false,
16+
});
17+
18+
export function ErrorBoundariesProvider({
19+
children,
20+
active = true,
21+
value,
22+
}: {
23+
children: React.ReactNode;
24+
active?: boolean;
25+
value?: ErrorBoundariesContextValue;
26+
}) {
27+
return (
28+
<ErrorBoundariesContext.Provider value={{ ...value, errorBoundariesActive: active }}>
29+
{children}
30+
</ErrorBoundariesContext.Provider>
31+
);
32+
}

src/error-boundary/index.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
'use client';
5+
import React from 'react';
6+
7+
import { ErrorBoundaryProps } from './interfaces';
8+
import InternalErrorBoundary from './internal';
9+
10+
export { ErrorBoundaryProps };
11+
12+
export default function ErrorBoundary(props: ErrorBoundaryProps) {
13+
return <InternalErrorBoundary {...props} />;
14+
}

0 commit comments

Comments
 (0)