From 6558a011adf51b68b25c1e6472b66543d0b88785 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Fri, 31 Oct 2025 18:01:01 +0100 Subject: [PATCH 1/4] feat: Method onToggleFocusMode for the left drawer --- .../external-global-left-panel-widget.tsx | 4 +++ .../runtime-drawers-widgetized.test.tsx | 25 +++++++++++++++++++ .../visual-refresh-toolbar/interfaces.ts | 1 + .../state/use-ai-drawer.ts | 12 ++++++++- src/internal/plugins/widget/interfaces.ts | 1 + 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pages/app-layout/utils/external-global-left-panel-widget.tsx b/pages/app-layout/utils/external-global-left-panel-widget.tsx index 5283d3362f..33ca05802b 100644 --- a/pages/app-layout/utils/external-global-left-panel-widget.tsx +++ b/pages/app-layout/utils/external-global-left-panel-widget.tsx @@ -139,4 +139,8 @@ registerLeftDrawer({ onHeaderActionClick: ({ detail }) => { console.log('onHeaderActionClick: ', detail); }, + + onToggleFocusMode: ({ detail }) => { + console.log('onToggleFocusMode: ', detail); + }, }); diff --git a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx index bbf9714607..7f80f87403 100644 --- a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx @@ -257,6 +257,31 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, ({ size }) => { } ); + test(`calls onToggleFocusMode handler by entering / exiting focus mode in left runtime drawer)`, () => { + const drawerId = 'global-drawer'; + const onToggleFocusMode = jest.fn(); + awsuiWidgetPlugins.registerLeftDrawer({ + ...drawerDefaults, + id: drawerId, + isExpandable: true, + onToggleFocusMode: event => onToggleFocusMode(event.detail), + }); + const renderProps = renderComponent(); + const { globalDrawersWrapper } = renderProps; + + globalDrawersWrapper.findAiDrawerTrigger()!.click(); + if (size === 'mobile') { + expect(globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)).toBeFalsy(); + } else { + createWrapper().findButtonGroup()!.findButtonById('expand')!.click(); + expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); + expect(onToggleFocusMode).toHaveBeenCalledWith({ isExpanded: true }); + createWrapper().findButtonGroup()!.findButtonById('expand')!.click(); + expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(false); + expect(onToggleFocusMode).toHaveBeenCalledWith({ isExpanded: false }); + } + }); + describe('metrics', () => { let sendPanoramaMetricSpy: jest.SpyInstance; beforeEach(() => { diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index d5ed39095a..5d91283079 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -27,6 +27,7 @@ export type InternalDrawer = AppLayoutProps.Drawer & { headerActions?: ReadonlyArray; onHeaderActionClick?: NonCancelableEventHandler; position?: 'side' | 'bottom'; + onToggleFocusMode?: NonCancelableEventHandler<{ isExpanded: boolean }>; }; // Widgetization notice: structures in this file are shared multiple app layout instances, possibly different minor versions. diff --git a/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts b/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts index 3ad144ff5b..ec3d84e762 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts +++ b/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts @@ -1,8 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { fireNonCancelableEvent } from '../../../internal/events'; +import { usePrevious } from '../../../internal/hooks/use-previous'; import { DrawerPayload as RuntimeAiDrawerConfig, WidgetMessage } from '../../../internal/plugins/widget/interfaces'; import { getLimitedValue } from '../../../split-panel/utils/size-utils'; import { mapRuntimeConfigToAiDrawer } from '../../runtime-drawer'; @@ -33,6 +34,15 @@ export function useAiDrawer({ const [size, setSize] = useState(null); const aiDrawerWasOpenRef = useRef(false); aiDrawerWasOpenRef.current = aiDrawerWasOpenRef.current || !!activeAiDrawerId; + const prevExpandedDrawerId = usePrevious(expandedDrawerId); + + useEffect(() => { + if (prevExpandedDrawerId !== expandedDrawerId) { + fireNonCancelableEvent(runtimeDrawer?.onToggleFocusMode, { + isExpanded: !!expandedDrawerId, + }); + } + }, [runtimeDrawer?.onToggleFocusMode, expandedDrawerId, prevExpandedDrawerId]); function onActiveAiDrawerResize(size: number) { const limitedSize = getLimitedValue(minAiDrawerSize, size, getMaxAiDrawerSize()); diff --git a/src/internal/plugins/widget/interfaces.ts b/src/internal/plugins/widget/interfaces.ts index 5ffcaf7431..e9a919eee7 100644 --- a/src/internal/plugins/widget/interfaces.ts +++ b/src/internal/plugins/widget/interfaces.ts @@ -50,6 +50,7 @@ export interface DrawerPayload { unmountHeader?: (container: HTMLElement) => void; headerActions?: ReadonlyArray; onHeaderActionClick?: NonCancelableEventHandler; + onToggleFocusMode?: NonCancelableEventHandler<{ isExpanded: boolean }>; position?: 'side' | 'bottom'; } From 1bbf47bf752c201d469b16ab69d9534e74873d96 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Mon, 3 Nov 2025 18:03:31 +0100 Subject: [PATCH 2/4] chore: refined the condition --- .../visual-refresh-toolbar/state/use-ai-drawer.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts b/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts index ec3d84e762..c8c7428b11 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts +++ b/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts @@ -36,13 +36,15 @@ export function useAiDrawer({ aiDrawerWasOpenRef.current = aiDrawerWasOpenRef.current || !!activeAiDrawerId; const prevExpandedDrawerId = usePrevious(expandedDrawerId); + const aiDrawer = runtimeDrawer && mapRuntimeConfigToAiDrawer(runtimeDrawer); + useEffect(() => { - if (prevExpandedDrawerId !== expandedDrawerId) { + if (prevExpandedDrawerId !== expandedDrawerId && (expandedDrawerId === aiDrawer?.id || expandedDrawerId === null)) { fireNonCancelableEvent(runtimeDrawer?.onToggleFocusMode, { isExpanded: !!expandedDrawerId, }); } - }, [runtimeDrawer?.onToggleFocusMode, expandedDrawerId, prevExpandedDrawerId]); + }, [runtimeDrawer?.onToggleFocusMode, expandedDrawerId, prevExpandedDrawerId, aiDrawer]); function onActiveAiDrawerResize(size: number) { const limitedSize = getLimitedValue(minAiDrawerSize, size, getMaxAiDrawerSize()); @@ -96,7 +98,6 @@ export function useAiDrawer({ } } - const aiDrawer = runtimeDrawer && mapRuntimeConfigToAiDrawer(runtimeDrawer); const activeAiDrawer = activeAiDrawerId && activeAiDrawerId === aiDrawer?.id ? aiDrawer : null; const activeAiDrawerSize = activeAiDrawerId ? (size ?? activeAiDrawer?.defaultSize ?? MIN_DRAWER_SIZE) : 0; const minAiDrawerSize = Math.min(activeAiDrawer?.defaultSize ?? MIN_DRAWER_SIZE, MIN_DRAWER_SIZE); From 68c491cd7df465120edca0804a9fe45e72f58c0b Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 4 Nov 2025 17:41:19 +0100 Subject: [PATCH 3/4] chore: Refactoring --- src/app-layout/runtime-drawer/index.tsx | 1 + .../visual-refresh-toolbar/state/use-ai-drawer.ts | 12 +----------- .../visual-refresh-toolbar/state/use-app-layout.tsx | 12 +++++++++++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/app-layout/runtime-drawer/index.tsx b/src/app-layout/runtime-drawer/index.tsx index deb1eb29c7..63d932c506 100644 --- a/src/app-layout/runtime-drawer/index.tsx +++ b/src/app-layout/runtime-drawer/index.tsx @@ -149,6 +149,7 @@ export const mapRuntimeConfigToAiDrawer = ( onToggle?: NonCancelableEventHandler; headerActions?: ReadonlyArray; exitExpandedModeTrigger?: React.ReactNode; + onToggleFocusMode?: NonCancelableEventHandler<{ isExpanded: boolean }>; } => { const { mountContent, unmountContent, trigger, exitExpandedModeTrigger, ...runtimeDrawer } = runtimeConfig; diff --git a/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts b/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts index c8c7428b11..ac2c2b9ba0 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts +++ b/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts @@ -1,9 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { fireNonCancelableEvent } from '../../../internal/events'; -import { usePrevious } from '../../../internal/hooks/use-previous'; import { DrawerPayload as RuntimeAiDrawerConfig, WidgetMessage } from '../../../internal/plugins/widget/interfaces'; import { getLimitedValue } from '../../../split-panel/utils/size-utils'; import { mapRuntimeConfigToAiDrawer } from '../../runtime-drawer'; @@ -34,18 +33,9 @@ export function useAiDrawer({ const [size, setSize] = useState(null); const aiDrawerWasOpenRef = useRef(false); aiDrawerWasOpenRef.current = aiDrawerWasOpenRef.current || !!activeAiDrawerId; - const prevExpandedDrawerId = usePrevious(expandedDrawerId); const aiDrawer = runtimeDrawer && mapRuntimeConfigToAiDrawer(runtimeDrawer); - useEffect(() => { - if (prevExpandedDrawerId !== expandedDrawerId && (expandedDrawerId === aiDrawer?.id || expandedDrawerId === null)) { - fireNonCancelableEvent(runtimeDrawer?.onToggleFocusMode, { - isExpanded: !!expandedDrawerId, - }); - } - }, [runtimeDrawer?.onToggleFocusMode, expandedDrawerId, prevExpandedDrawerId, aiDrawer]); - function onActiveAiDrawerResize(size: number) { const limitedSize = getLimitedValue(minAiDrawerSize, size, getMaxAiDrawerSize()); setSize(limitedSize); 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 2590968f34..48a5e7f184 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 @@ -71,7 +71,7 @@ export const useAppLayout = ( const [navigationAnimationDisabled, setNavigationAnimationDisabled] = useState(true); const [splitPanelAnimationDisabled, setSplitPanelAnimationDisabled] = useState(true); const [isNested, setIsNested] = useState(false); - const [expandedDrawerId, setExpandedDrawerId] = useState(null); + const [expandedDrawerId, setInternalExpandedDrawerId] = useState(null); const rootRefInternal = useRef(null); // This workaround ensures the ref is defined before checking if the app layout is nested. // On initial render, the ref might be undefined because this component loads asynchronously via the widget API. @@ -90,6 +90,16 @@ export const useAppLayout = ( fireNonCancelableEvent(onToolsChange, { open }); }; + const setExpandedDrawerId = (value: string | null) => { + setInternalExpandedDrawerId(value); + + if (aiDrawer?.onToggleFocusMode && (value === aiDrawer?.id || value === null)) { + fireNonCancelableEvent(aiDrawer.onToggleFocusMode, { + isExpanded: !!value, + }); + } + }; + const onGlobalDrawerFocus = (drawerId: string, open: boolean) => { globalDrawersFocusControl.setFocus({ force: true, drawerId, open }); }; From 4e3b084ccfd45233b4a3bf705d3c6a0c355296c2 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 4 Nov 2025 17:48:41 +0100 Subject: [PATCH 4/4] chore: Cleanup --- src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts b/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts index ac2c2b9ba0..3ad144ff5b 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts +++ b/src/app-layout/visual-refresh-toolbar/state/use-ai-drawer.ts @@ -34,8 +34,6 @@ export function useAiDrawer({ const aiDrawerWasOpenRef = useRef(false); aiDrawerWasOpenRef.current = aiDrawerWasOpenRef.current || !!activeAiDrawerId; - const aiDrawer = runtimeDrawer && mapRuntimeConfigToAiDrawer(runtimeDrawer); - function onActiveAiDrawerResize(size: number) { const limitedSize = getLimitedValue(minAiDrawerSize, size, getMaxAiDrawerSize()); setSize(limitedSize); @@ -88,6 +86,7 @@ export function useAiDrawer({ } } + const aiDrawer = runtimeDrawer && mapRuntimeConfigToAiDrawer(runtimeDrawer); const activeAiDrawer = activeAiDrawerId && activeAiDrawerId === aiDrawer?.id ? aiDrawer : null; const activeAiDrawerSize = activeAiDrawerId ? (size ?? activeAiDrawer?.defaultSize ?? MIN_DRAWER_SIZE) : 0; const minAiDrawerSize = Math.min(activeAiDrawer?.defaultSize ?? MIN_DRAWER_SIZE, MIN_DRAWER_SIZE);