diff --git a/pages/app-layout/utils/external-widget.tsx b/pages/app-layout/utils/external-widget.tsx index a81ca17896..ea64dae6c0 100644 --- a/pages/app-layout/utils/external-widget.tsx +++ b/pages/app-layout/utils/external-widget.tsx @@ -94,40 +94,11 @@ const Counter: React.FC = ({ children }) => { ); }; -class CounterStateManager { - private isPaused = false; - private pauseCallback: ((isPaused: boolean) => void) | null = null; - - private onPauseStateChange() { - if (this.pauseCallback) { - this.pauseCallback(this.isPaused); - } - } - - registerPauseCallback(callback: (isPaused: boolean) => void) { - this.pauseCallback = callback; - } - - unregisterPauseCallback() { - this.pauseCallback = null; - } - - pause() { - this.isPaused = true; - this.onPauseStateChange(); - } - - resume() { - this.isPaused = false; - this.onPauseStateChange(); - } -} - -const autoIncrementState = new CounterStateManager(); - -const AutoIncrementCounter: React.FC = ({ children }) => { +const AutoIncrementCounter: React.FC<{ + onVisibilityChange?: (callback: (isVisible: boolean) => void) => () => void; +}> = ({ children, onVisibilityChange }) => { const [count, setCount] = useState(0); - const [isPaused, setIsPaused] = useState(false); + const [isPaused, setIsPaused] = useState(true); useEffect(() => { const interval = setInterval(() => { @@ -140,12 +111,16 @@ const AutoIncrementCounter: React.FC = ({ children }) => { }, [isPaused]); useEffect(() => { - autoIncrementState.registerPauseCallback(isPaused => { - setIsPaused(isPaused); - }); - - return () => autoIncrementState.unregisterPauseCallback(); - }, []); + if (onVisibilityChange) { + const unsubscribe = onVisibilityChange((isVisible: boolean) => { + setIsPaused(!isVisible); + }); + + return () => { + unsubscribe(); + }; + } + }, [onVisibilityChange]); return (
@@ -163,8 +138,6 @@ awsuiPlugins.appLayout.registerDrawer({ resizable: true, defaultSize: 350, preserveInactiveContent: true, - onShow: () => autoIncrementState.resume(), - onHide: () => autoIncrementState.pause(), ariaLabels: { closeButton: 'Close button', @@ -184,8 +157,16 @@ awsuiPlugins.appLayout.registerDrawer({ console.log('resize', event.detail); }, - mountContent: container => { - ReactDOM.render(global widget content circle 1, container); + mountContent: ( + container: HTMLElement, + onVisibilityChange?: (callback: (isVisible: boolean) => void) => () => void + ) => { + ReactDOM.render( + + global widget content circle 1 + , + container + ); }, unmountContent: container => unmountComponentAtNode(container), }); diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index a6357fd2c9..3cd077da93 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -1065,17 +1065,26 @@ describe('toolbar mode only features', () => { expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); }); - test('should call onShow and onHide when global drawer with preserveInactiveContent is opened and closed', async () => { + test('should call visibilityChange callback when global drawer with preserveInactiveContent is opened and closed', async () => { const onShow = jest.fn(); const onHide = jest.fn(); awsuiPlugins.appLayout.registerDrawer({ ...drawerDefaults, id: 'global-drawer-1', type: 'global', - mountContent: container => (container.textContent = 'global drawer content 1'), + mountContent: (container, onVisibilityChange) => { + if (onVisibilityChange) { + onVisibilityChange((isVisible: boolean) => { + if (isVisible) { + onShow(); + } else { + onHide(); + } + }); + } + container.textContent = 'global drawer content 1'; + }, preserveInactiveContent: true, - onShow, - onHide, }); const { wrapper, globalDrawersWrapper } = await renderComponent(); diff --git a/src/app-layout/interfaces.ts b/src/app-layout/interfaces.ts index 362e611bd3..0f48c32ba2 100644 --- a/src/app-layout/interfaces.ts +++ b/src/app-layout/interfaces.ts @@ -308,8 +308,6 @@ export namespace AppLayoutProps { defaultSize?: number; onResize?: NonCancelableEventHandler<{ size: number }>; preserveInactiveContent?: boolean; - onShow?: NonCancelableEventHandler; - onHide?: NonCancelableEventHandler; } export interface DrawerAriaLabels { diff --git a/src/app-layout/runtime-api.tsx b/src/app-layout/runtime-api.tsx index 2f3d751298..d37945bc6d 100644 --- a/src/app-layout/runtime-api.tsx +++ b/src/app-layout/runtime-api.tsx @@ -2,10 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { fireNonCancelableEvent } from '../internal/events'; +import { fireNonCancelableEvent, NonCancelableEventHandler } from '../internal/events'; import { DrawerConfig as RuntimeDrawerConfig } from '../internal/plugins/controllers/drawers'; import { RuntimeContentWrapper } from '../internal/plugins/helpers'; import { sortByPriority } from '../internal/plugins/helpers/utils'; +import VisibilityStateManager from '../internal/plugins/helpers/visibility-state-manager'; import { AppLayoutProps } from './interfaces'; export interface DrawersLayout { @@ -13,6 +14,8 @@ export interface DrawersLayout { after: Array; } +const visibilityStateManagerMap = new Map(); + export function convertRuntimeDrawers(drawers: Array): DrawersLayout { const converted = drawers.map( ({ @@ -20,22 +23,43 @@ export function convertRuntimeDrawers(drawers: Array): Draw unmountContent, trigger, ...runtimeDrawer - }): AppLayoutProps.Drawer & { orderPriority?: number } => ({ - ...runtimeDrawer, - ariaLabels: { drawerName: runtimeDrawer.ariaLabels.content ?? '', ...runtimeDrawer.ariaLabels }, - trigger: { - iconSvg: ( - // eslint-disable-next-line react/no-danger - + }): AppLayoutProps.Drawer & { + orderPriority?: number; + onShow?: NonCancelableEventHandler; + onHide?: NonCancelableEventHandler; + } => { + let visibilityStateManager: VisibilityStateManager; + if (visibilityStateManagerMap.has(runtimeDrawer.id)) { + visibilityStateManager = visibilityStateManagerMap.get(runtimeDrawer.id)!; + } else { + visibilityStateManager = new VisibilityStateManager(); + visibilityStateManagerMap.set(runtimeDrawer.id, visibilityStateManager); + } + + return { + ...runtimeDrawer, + ariaLabels: { drawerName: runtimeDrawer.ariaLabels.content ?? '', ...runtimeDrawer.ariaLabels }, + trigger: { + iconSvg: ( + // eslint-disable-next-line react/no-danger + + ), + }, + content: ( + ), - }, - content: ( - - ), - onResize: event => { - fireNonCancelableEvent(runtimeDrawer.onResize, { size: event.detail.size, id: runtimeDrawer.id }); - }, - }) + onResize: event => { + fireNonCancelableEvent(runtimeDrawer.onResize, { size: event.detail.size, id: runtimeDrawer.id }); + }, + onShow: visibilityStateManager.show, + onHide: visibilityStateManager.hide, + }; + } ); const sorted = sortByPriority(converted); return { diff --git a/src/app-layout/visual-refresh-toolbar/drawer/index.tsx b/src/app-layout/visual-refresh-toolbar/drawer/index.tsx index 275abe4a23..aa1fa5d2b6 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/index.tsx @@ -5,7 +5,7 @@ import clsx from 'clsx'; import { InternalButton } from '../../../button/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; -import { fireNonCancelableEvent } from '../../../internal/events'; +import { fireNonCancelableEvent, NonCancelableEventHandler } from '../../../internal/events'; import customCssProps from '../../../internal/generated/custom-css-properties'; import { usePrevious } from '../../../internal/hooks/use-previous'; import { createWidgetizedComponent } from '../../../internal/widgets'; @@ -131,7 +131,9 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD interface AppLayoutGlobalDrawerImplementationProps { appLayoutInternals: AppLayoutInternals; show: boolean; - activeGlobalDrawer: AppLayoutProps.Drawer | undefined; + activeGlobalDrawer: + | (AppLayoutProps.Drawer & { onShow?: NonCancelableEventHandler; onHide?: NonCancelableEventHandler }) + | undefined; } export function AppLayoutGlobalDrawerImplementation({ diff --git a/src/internal/plugins/controllers/drawers.ts b/src/internal/plugins/controllers/drawers.ts index 2bbd9f75e1..277af9eba7 100644 --- a/src/internal/plugins/controllers/drawers.ts +++ b/src/internal/plugins/controllers/drawers.ts @@ -21,11 +21,12 @@ export interface DrawerConfig { trigger: { iconSvg: string; }; - mountContent: (container: HTMLElement) => void; + mountContent: ( + container: HTMLElement, + onVisibilityChange?: (callback: (isVisible: boolean) => void) => () => void + ) => void; unmountContent: (container: HTMLElement) => void; preserveInactiveContent?: boolean; - onShow?: NonCancelableEventHandler; - onHide?: NonCancelableEventHandler; } export type UpdateDrawerConfig = Pick; diff --git a/src/internal/plugins/helpers/runtime-content-wrapper.tsx b/src/internal/plugins/helpers/runtime-content-wrapper.tsx index df18772e8b..80565a56e5 100644 --- a/src/internal/plugins/helpers/runtime-content-wrapper.tsx +++ b/src/internal/plugins/helpers/runtime-content-wrapper.tsx @@ -2,18 +2,27 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useEffect, useRef } from 'react'; +type VisibilityCallback = (isVisible: boolean) => void; + interface RuntimeContentWrapperProps { - mountContent: (container: HTMLElement) => void; + mountContent: (container: HTMLElement, visibilityCallback?: (callback: VisibilityCallback) => () => void) => void; unmountContent: (container: HTMLElement) => void; + registerVisibilityCallback?: (callback: VisibilityCallback) => () => void; } -export function RuntimeContentWrapper({ mountContent, unmountContent }: RuntimeContentWrapperProps) { +export function RuntimeContentWrapper({ + mountContent, + unmountContent, + registerVisibilityCallback, +}: RuntimeContentWrapperProps) { const ref = useRef(null); useEffect(() => { const container = ref.current!; - mountContent(container); - return () => unmountContent(container); + mountContent(container, registerVisibilityCallback); + return () => { + unmountContent(container); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/internal/plugins/helpers/visibility-state-manager.ts b/src/internal/plugins/helpers/visibility-state-manager.ts new file mode 100644 index 0000000000..cd82b87929 --- /dev/null +++ b/src/internal/plugins/helpers/visibility-state-manager.ts @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * This class is used to manage the visibility state of the runtime drawers. + */ +export default class VisibilityStateManager { + isVisible = false; + visibilityCallback: ((isPaused: boolean) => void) | null = null; + + private onVisibleStateChange = () => { + if (this.visibilityCallback) { + this.visibilityCallback(this.isVisible); + } + }; + + registerVisibilityCallback = (callback: (isVisible: boolean) => void) => { + this.visibilityCallback = callback; + + return () => { + this.visibilityCallback = null; + }; + }; + + show = () => { + this.isVisible = true; + this.onVisibleStateChange(); + }; + + hide = () => { + this.isVisible = false; + this.onVisibleStateChange(); + }; +}