diff --git a/components/src/index.ts b/components/src/index.ts index 0877f363..f8587687 100644 --- a/components/src/index.ts +++ b/components/src/index.ts @@ -1 +1,9 @@ export * from './web-components'; + +export { type ErrorEvent, UserFacingError } from './preact/components/error-display'; + +declare global { + interface HTMLElementEventMap { + 'gs-error': ErrorEvent; + } +} diff --git a/components/src/preact/components/error-boundary.stories.tsx b/components/src/preact/components/error-boundary.stories.tsx index 6f5eb672..3a06ecaa 100644 --- a/components/src/preact/components/error-boundary.stories.tsx +++ b/components/src/preact/components/error-boundary.stories.tsx @@ -1,12 +1,17 @@ +import { withActions } from '@storybook/addon-actions/decorator'; import { type Meta, type StoryObj } from '@storybook/preact'; import { expect, waitFor, within } from '@storybook/test'; import { ErrorBoundary } from './error-boundary'; +import { GS_ERROR_EVENT_TYPE, UserFacingError } from './error-display'; const meta: Meta = { title: 'Component/Error boundary', component: ErrorBoundary, - parameters: { fetchMock: {} }, + parameters: { + fetchMock: {}, + actions: { handles: [GS_ERROR_EVENT_TYPE] }, + }, argTypes: { size: { control: 'object' }, defaultSize: { control: 'object' }, @@ -14,6 +19,7 @@ const meta: Meta = { args: { size: { height: '600px', width: '100%' }, }, + decorators: [withActions], }; export default meta; @@ -34,7 +40,21 @@ export const ErrorBoundaryWithoutErrorStory: StoryObj = { export const ErrorBoundaryWithErrorStory: StoryObj = { render: (args) => ( - + new Error('Some error')} /> + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const content = canvas.queryByText('Some content.', { exact: false }); + await waitFor(() => expect(content).not.toBeInTheDocument()); + await waitFor(() => expect(canvas.getByText('Error')).toBeInTheDocument()); + }, +}; + +export const ErrorBoundaryWithUserFacingErrorStory: StoryObj = { + render: (args) => ( + + new UserFacingError('Error Headline', 'Some error')} /> ), play: async ({ canvasElement }) => { @@ -45,6 +65,6 @@ export const ErrorBoundaryWithErrorStory: StoryObj = { }, }; -const ContentThatThrowsError = () => { - throw new Error('Some error'); +const ContentThatThrowsError = (props: { error: () => Error }) => { + throw props.error(); }; diff --git a/components/src/preact/components/error-display.tsx b/components/src/preact/components/error-display.tsx index a182fb4e..4ba4dc60 100644 --- a/components/src/preact/components/error-display.tsx +++ b/components/src/preact/components/error-display.tsx @@ -1,5 +1,11 @@ import { type FunctionComponent } from 'preact'; -import { useRef } from 'preact/hooks'; +import { useEffect, useRef } from 'preact/hooks'; + +export const GS_ERROR_EVENT_TYPE = 'gs-error'; + +export interface ErrorEvent extends Event { + readonly error: Error; +} export class UserFacingError extends Error { constructor( @@ -14,10 +20,24 @@ export class UserFacingError extends Error { export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => { console.error(error); + const containerRef = useRef(null); const ref = useRef(null); + useEffect(() => { + containerRef.current?.dispatchEvent( + new ErrorEvent(GS_ERROR_EVENT_TYPE, { + error, + bubbles: true, + composed: true, + }), + ); + }); + return ( -
+
Error
Oops! Something went wrong. diff --git a/components/src/web-components/errorHandling.mdx b/components/src/web-components/errorHandling.mdx new file mode 100644 index 00000000..0c5d1525 --- /dev/null +++ b/components/src/web-components/errorHandling.mdx @@ -0,0 +1,8 @@ +import { Meta } from '@storybook/blocks'; + + + +# Error Handling + +All components dispatch a `gs-error` event of type `ErrorEvent` when an error occurs. +The event contains an `error` property that holds the `Error` object.