diff --git a/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap b/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap index 776ae9c761..a0b1a5fdf2 100644 --- a/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap +++ b/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap @@ -1365,6 +1365,8 @@ Map { "children":
notifications
, + "flashbarProps": null, + "setFlashbarProps": [Function], }, "AppLayoutSplitPanelDrawerBottomImplementation" => { "appLayoutInternals": { diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index 968a690e95..6204f7b225 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -5,6 +5,7 @@ import React from 'react'; import { BreadcrumbGroupProps } from '../../breadcrumb-group/interfaces'; import { ButtonGroupProps } from '../../button-group/interfaces'; +import { FlashbarProps } from '../../flashbar/interfaces'; import { SplitPanelSideToggleProps } from '../../internal/context/split-panel-context'; import { NonCancelableEventHandler } from '../../internal/events'; import { SomeOptional } from '../../internal/types'; @@ -90,6 +91,8 @@ interface AppLayoutWidgetizedState extends AppLayoutInternals { verticalOffsets: VerticalLayoutOutput; navigationAnimationDisabled: boolean; aiDrawerExpandedMode: boolean; + flashbarProps: FlashbarProps | null; + setFlashbarProps: (props: FlashbarProps | null) => void; splitPanelOffsets: { stickyVerticalBottomOffset: number; mainContentPaddingBlockEnd: number | undefined; diff --git a/src/app-layout/visual-refresh-toolbar/notifications/index.tsx b/src/app-layout/visual-refresh-toolbar/notifications/index.tsx index b9d7e2d601..152a206b22 100644 --- a/src/app-layout/visual-refresh-toolbar/notifications/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/notifications/index.tsx @@ -5,20 +5,26 @@ import clsx from 'clsx'; import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal'; +import { FlashbarImplementation } from '../../../flashbar/implementation'; import { highContrastHeaderClassName } from '../../../internal/utils/content-header-utils'; -import { AppLayoutInternals } from '../interfaces'; +import { AppLayoutInternals, AppLayoutState } from '../interfaces'; import { NotificationsSlot } from '../skeleton/slots'; +import { FlashbarPropsSetter } from '../state/runtime-notifications'; import testutilStyles from '../../test-classes/styles.css.js'; import styles from './styles.css.js'; export interface AppLayoutNotificationsImplementationProps { appLayoutInternals: AppLayoutInternals; + flashbarProps?: AppLayoutState['widgetizedState']['flashbarProps']; + setFlashbarProps?: AppLayoutState['widgetizedState']['setFlashbarProps']; children: React.ReactNode; } export function AppLayoutNotificationsImplementation({ appLayoutInternals, + flashbarProps, + setFlashbarProps, children, }: AppLayoutNotificationsImplementationProps) { const { ariaLabels, stickyNotifications, setNotificationsHeight, verticalOffsets } = appLayoutInternals; @@ -51,7 +57,8 @@ export function AppLayoutNotificationsImplementation({ }} >
- {children} + {children} + {flashbarProps && }
); diff --git a/src/app-layout/visual-refresh-toolbar/state/runtime-notifications.ts b/src/app-layout/visual-refresh-toolbar/state/runtime-notifications.ts new file mode 100644 index 0000000000..7e6b2535ac --- /dev/null +++ b/src/app-layout/visual-refresh-toolbar/state/runtime-notifications.ts @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { createContext, useState } from 'react'; + +import { FlashbarProps } from '../../../flashbar/interfaces'; +import { WidgetMessage } from '../../../internal/plugins/widget/interfaces'; + +export const FlashbarPropsSetter = createContext<((props: FlashbarProps | null) => void) | null>(null); + +export function useRuntimeNotifications() { + const [flashbarProps, setFlashbarProps] = useState(null); + const [notifications, setNotifications] = useState>([]); + + function notificationsMessageHandler(message: WidgetMessage) { + if (message.type === 'emitNotification') { + setNotifications(notifications => [...notifications, message.payload]); + } + } + + return { + flashbarProps: + flashbarProps || notifications.length > 0 + ? { + ...flashbarProps, + items: [...(flashbarProps?.items ?? []), ...notifications], + } + : null, + setFlashbarProps, + notificationsMessageHandler, + }; +} diff --git a/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx b/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx index e1e47549d0..499e35ea4c 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx +++ b/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx @@ -26,6 +26,7 @@ import { } from '../compute-layout'; import { AppLayoutState } from '../interfaces'; import { AppLayoutInternalProps, AppLayoutInternals } from '../interfaces'; +import { useRuntimeNotifications } from './runtime-notifications'; import { useAiDrawer } from './use-ai-drawer'; import { useWidgetMessages } from './use-widget-messages'; @@ -158,7 +159,11 @@ export const useAppLayout = ( expandedDrawerId, setExpandedDrawerId, }); - useWidgetMessages(hasToolbar, message => aiDrawerMessageHandler(message)); + const { flashbarProps, setFlashbarProps, notificationsMessageHandler } = useRuntimeNotifications(); + useWidgetMessages(hasToolbar, message => { + aiDrawerMessageHandler(message); + notificationsMessageHandler(message); + }); const aiDrawerFocusControl = useAsyncFocusControl(!!activeAiDrawer?.id, true, activeAiDrawer?.id); const onActiveDrawerChangeHandler = ( @@ -469,6 +474,8 @@ export const useAppLayout = ( widgetizedState: { ...appLayoutInternals, aiDrawerExpandedMode: expandedDrawerId === activeAiDrawer?.id, + flashbarProps, + setFlashbarProps, isNested, navigationAnimationDisabled, verticalOffsets, diff --git a/src/app-layout/visual-refresh-toolbar/widget-areas/top-content-slot.tsx b/src/app-layout/visual-refresh-toolbar/widget-areas/top-content-slot.tsx index 5265418cab..8d8cf4a4f0 100644 --- a/src/app-layout/visual-refresh-toolbar/widget-areas/top-content-slot.tsx +++ b/src/app-layout/visual-refresh-toolbar/widget-areas/top-content-slot.tsx @@ -26,7 +26,11 @@ export const TopContentSlotImplementation = ({ appLayoutProps, appLayoutState }: > )} {appLayoutProps.notifications && ( - + {appLayoutProps.notifications} )} diff --git a/src/flashbar/implementation.tsx b/src/flashbar/implementation.tsx index 9bf77cc624..cc43acecf7 100644 --- a/src/flashbar/implementation.tsx +++ b/src/flashbar/implementation.tsx @@ -1,13 +1,34 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useContext, useEffect } from 'react'; +import { FlashbarPropsSetter } from '../app-layout/visual-refresh-toolbar/state/runtime-notifications'; import { createWidgetizedComponent } from '../internal/widgets'; import CollapsibleFlashbar from './collapsible-flashbar'; -import { InternalFlashbarProps } from './interfaces'; +import { FlashbarProps, InternalFlashbarProps } from './interfaces'; import NonCollapsibleFlashbar from './non-collapsible-flashbar'; +function FlashbarPropagator({ + props, + setFlashbarProps, +}: { + props: FlashbarProps; + setFlashbarProps: (props: FlashbarProps | null) => void; +}) { + useEffect(() => { + setFlashbarProps(props); + return () => setFlashbarProps(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +} + export function FlashbarImplementation(props: InternalFlashbarProps) { + const setFlashbarProps = useContext(FlashbarPropsSetter); + if (setFlashbarProps) { + return ; + } if (props.stackItems) { return ; } else { diff --git a/src/internal/plugins/widget.ts b/src/internal/plugins/widget.ts index 45d6fa4508..b75719511d 100644 --- a/src/internal/plugins/widget.ts +++ b/src/internal/plugins/widget.ts @@ -2,4 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export * from './widget/interfaces'; export { isAppLayoutReady, whenAppLayoutReady } from './widget/core'; -export { registerLeftDrawer, updateDrawer } from './widget/index'; +export { registerLeftDrawer, updateDrawer, emitNotification } from './widget/index'; diff --git a/src/internal/plugins/widget/index.ts b/src/internal/plugins/widget/index.ts index 1a0ef52991..7b63b41110 100644 --- a/src/internal/plugins/widget/index.ts +++ b/src/internal/plugins/widget/index.ts @@ -2,7 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { getAppLayoutMessageHandler, pushInitialMessage } from './core'; -import { AppLayoutUpdateMessage, DrawerPayload, RegisterDrawerMessage } from './interfaces'; +import { + AppLayoutUpdateMessage, + DrawerPayload, + EmitNotificationMessage, + EmitNotificationPayload, + RegisterDrawerMessage, +} from './interfaces'; /** * Registers a new runtime drawer to app layout @@ -21,3 +27,13 @@ export function registerLeftDrawer(drawer: DrawerPayload) { export function updateDrawer(message: AppLayoutUpdateMessage) { getAppLayoutMessageHandler()?.(message); } + +/** + * Emit a notification to the app layout + * @param payload + */ +export function emitNotification(payload: EmitNotificationPayload) { + const message: EmitNotificationMessage = { type: 'emitNotification', payload }; + pushInitialMessage(message); + getAppLayoutMessageHandler()?.(message); +} diff --git a/src/internal/plugins/widget/interfaces.ts b/src/internal/plugins/widget/interfaces.ts index d74ed6233b..a024ba069c 100644 --- a/src/internal/plugins/widget/interfaces.ts +++ b/src/internal/plugins/widget/interfaces.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ButtonGroupProps } from '../../../button-group/interfaces'; +import { FlashbarProps } from '../../../flashbar/interfaces'; import { NonCancelableEventHandler } from '../../events'; interface Message { @@ -70,6 +71,14 @@ export type AppLayoutUpdateMessage = | ExpandDrawerMessage | ExitExpandedModeMessage; -export type InitialMessage = RegisterDrawerMessage; +export interface EmitNotificationPayload { + type: FlashbarProps.Type; + header: string; + content: string; +} + +export type EmitNotificationMessage = Message<'emitNotification', EmitNotificationPayload>; + +export type InitialMessage = EmitNotificationMessage | RegisterDrawerMessage; export type WidgetMessage = InitialMessage | AppLayoutUpdateMessage;