From 7d200c1e1042cd7319c64591b02d312fa976351b Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Mon, 1 Sep 2025 15:12:39 +0200 Subject: [PATCH 01/12] feat: AI drawer header actions --- package.json | 2 +- .../external-global-left-panel-widget.tsx | 50 ++++++--- .../drawer/global-ai-drawer.tsx | 101 +++++++++++++----- .../visual-refresh-toolbar/interfaces.ts | 4 + src/internal/plugins/widget/interfaces.ts | 3 + 5 files changed, 113 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index f491041e74..42fbea0124 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "869 kB", + "limit": "887 kB", "ignore": "react-dom" } ], 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 445905672d..4757fa98e8 100644 --- a/pages/app-layout/utils/external-global-left-panel-widget.tsx +++ b/pages/app-layout/utils/external-global-left-panel-widget.tsx @@ -3,9 +3,7 @@ import React from 'react'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; -import { Box } from '~components'; -import Button from '~components/button'; -import ButtonDropdown from '~components/button-dropdown'; +import { Box, StatusIndicator } from '~components'; import { registerLeftDrawer } from '~components/internal/plugins/widget'; import styles from '../styles.scss'; @@ -69,20 +67,38 @@ registerLeftDrawer({ unmountContent: container => unmountComponentAtNode(container), mountHeader: container => { - ReactDOM.render( -
-
AI Panel
-
- -
-
, - container - ); + ReactDOM.render(
AI Panel
, container); }, unmountHeader: container => unmountComponentAtNode(container), + + headerActions: [ + { + type: 'menu-dropdown', + id: 'more-actions', + text: 'More actions', + items: [ + { + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + { + id: 'remove', + iconName: 'remove', + text: 'Remove', + }, + ], + }, + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + popoverFeedback: Message copied, + }, + ], + + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx index 7c37fa33bf..1023681daf 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx @@ -4,7 +4,8 @@ import React, { useRef } from 'react'; import { Transition } from 'react-transition-group'; import clsx from 'clsx'; -import { InternalButton } from '../../../button/internal'; +import { ButtonGroupProps } from '../../../button-group/interfaces'; +import ButtonGroup from '../../../button-group/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; import customCssProps from '../../../internal/generated/custom-css-properties'; import { usePrevious } from '../../../internal/hooks/use-previous'; @@ -90,6 +91,30 @@ export function AppLayoutGlobalAiDrawerImplementation({ // (window is between mobile and desktop sizes). At this point, the drawer can't be // resized in either direction, so we disable the resize handler const isResizingDisabled = maxAiDrawerSize < activeAiDrawerSize; + let drawerActions: ReadonlyArray = [ + { + type: 'icon-button', + id: 'expand', + iconName: isExpanded ? 'shrink' : 'expand', + text: activeAiDrawer?.ariaLabels?.expandedModeButton ?? '', + }, + { + type: 'icon-button', + id: 'close', + iconName: isMobile ? 'close' : 'angle-left', + text: computedAriaLabels.closeButton, + }, + ]; + if (activeAiDrawer?.headerActions) { + drawerActions = [ + { + type: 'group', + text: 'Vote', + items: activeAiDrawer.headerActions!, + }, + ...drawerActions, + ]; + } return ( @@ -152,34 +177,52 @@ export function AppLayoutGlobalAiDrawerImplementation({
{activeAiDrawer?.header ??
}
- {!isMobile && activeAiDrawer?.isExpandable && ( -
- setExpandedDrawerId(isExpanded ? null : activeDrawerId!)} - variant="icon" - analyticsAction={isExpanded ? 'expand' : 'collapse'} - /> -
- )} -
- onActiveAiDrawerChange?.(null, { initiatedByUserAction: true })} - ref={aiDrawerFocusControl?.refs.close} - variant="icon" - analyticsAction="close" - /> -
+ { + switch (event.detail.id) { + case 'close': + onActiveAiDrawerChange?.(null, { initiatedByUserAction: true }); + break; + case 'expand': + setExpandedDrawerId(isExpanded ? null : activeDrawerId!); + break; + default: + activeAiDrawer?.onHeaderActionClick?.(event); + } + }} + ariaLabel="Chat actions" + items={drawerActions} + /> + {/*{!isMobile && activeAiDrawer?.isExpandable && (*/} + {/*
*/} + {/* setExpandedDrawerId(isExpanded ? null : activeDrawerId!)}*/} + {/* variant="icon"*/} + {/* analyticsAction={isExpanded ? 'expand' : 'collapse'}*/} + {/* />*/} + {/*
*/} + {/*)}*/} + {/*
*/} + {/* onActiveAiDrawerChange?.(null, { initiatedByUserAction: true })}*/} + {/* ref={aiDrawerFocusControl?.refs.close}*/} + {/* variant="icon"*/} + {/* analyticsAction="close"*/} + {/* />*/} + {/*
*/}
{!isMobile && isExpanded && activeAiDrawer?.ariaLabels?.exitExpandedModeButton && ( diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index 509ba31cfa..968a690e95 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -4,7 +4,9 @@ import React from 'react'; import { BreadcrumbGroupProps } from '../../breadcrumb-group/interfaces'; +import { ButtonGroupProps } from '../../button-group/interfaces'; import { SplitPanelSideToggleProps } from '../../internal/context/split-panel-context'; +import { NonCancelableEventHandler } from '../../internal/events'; import { SomeOptional } from '../../internal/types'; import { AppLayoutProps, AppLayoutPropsWithDefaults } from '../interfaces'; import { SplitPanelProviderProps } from '../split-panel'; @@ -22,6 +24,8 @@ export type InternalDrawer = AppLayoutProps.Drawer & { isExpandable?: boolean; ariaLabels: AppLayoutProps.Drawer['ariaLabels'] & { expandedModeButton?: string; exitExpandedModeButton?: string }; header?: React.ReactNode; + headerActions?: ReadonlyArray; + onHeaderActionClick?: NonCancelableEventHandler; }; // Widgetization notice: structures in this file are shared multiple app layout instances, possibly different minor versions. diff --git a/src/internal/plugins/widget/interfaces.ts b/src/internal/plugins/widget/interfaces.ts index 3c3f00fc1b..24a4c870a8 100644 --- a/src/internal/plugins/widget/interfaces.ts +++ b/src/internal/plugins/widget/interfaces.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { ButtonGroupProps } from '../../../button-group/interfaces'; import { NonCancelableEventHandler } from '../../events'; interface Message { @@ -44,6 +45,8 @@ export interface DrawerPayload { onToggle?: NonCancelableEventHandler; mountHeader?: (container: HTMLElement) => void; unmountHeader?: (container: HTMLElement) => void; + headerActions?: ReadonlyArray; + onHeaderActionClick?: NonCancelableEventHandler; } export type RegisterDrawerMessage = Message<'registerLeftDrawer', DrawerPayload>; From 3f7c8bbedec53480bda3f25d9724ac371eff46e0 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Mon, 1 Sep 2025 17:54:01 +0200 Subject: [PATCH 02/12] fix: U tests --- .../runtime-drawers-widgetized.test.tsx | 2 +- .../__tests__/runtime-drawers.test.tsx | 107 ++++++++++++------ .../drawer/global-ai-drawer.tsx | 49 +++----- 3 files changed, 85 insertions(+), 73 deletions(-) diff --git a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx index 1bcd908e42..dcc32b9095 100644 --- a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx @@ -168,7 +168,7 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, ({ size }) => { if (size === 'mobile') { expect(globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerDefaults.id)).toBeFalsy(); } else { - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerDefaults.id)!.click(); + createWrapper().findButtonGroup()!.findButtonById('expand')!.click(); expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)!.isDrawerInExpandedMode()).toBe(true); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(true); globalDrawersWrapper.findLeaveExpandedModeButtonInAIDrawer()!.click(); diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index 55112c3250..5463b68650 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -50,6 +50,7 @@ async function renderComponent(jsx: React.ReactElement) { globalDrawersWrapper, rerender, getByTestId, + container, ...rest, }; } @@ -946,6 +947,16 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); } }; + const findCloseButtonByActiveDrawerId = ( + id: string, + renderProps: Awaited> + ) => { + if (type === 'global') { + return renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId(id); + } else { + return createWrapper(renderProps.container).findButtonGroup()!.findButtonById('close'); + } + }; test('renders resize handle for a global drawer when config is enabled', async () => { registerDrawer({ @@ -990,7 +1001,8 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer')!.getElement()).toBeInTheDocument(); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); + renderProps.debug(findCloseButtonByActiveDrawerId('global-drawer', renderProps)!.getElement()); + findCloseButtonByActiveDrawerId('global-drawer', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer')).toBeNull(); }); @@ -1074,7 +1086,7 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer-1', renderProps)!.focus(); findDrawerTriggerById('global-drawer-1', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + findCloseButtonByActiveDrawerId('global-drawer-1', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')).toBeNull(); await waitFor(() => { expect(findDrawerTriggerById('global-drawer-1', renderProps)!.getElement()).toHaveFocus(); @@ -1099,7 +1111,7 @@ describe('toolbar mode only features', () => { await delay(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + findCloseButtonByActiveDrawerId('global-drawer-1', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(false); @@ -1129,7 +1141,7 @@ describe('toolbar mode only features', () => { expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); expect(onVisibilityChangeMock).toHaveBeenCalledWith(true); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + findCloseButtonByActiveDrawerId('global-drawer-1', renderProps)!.click(); expect(onVisibilityChangeMock).toHaveBeenCalledWith(false); }); @@ -1183,7 +1195,7 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer', renderProps)!.click(); expect(onToggle).toHaveBeenCalledWith({ isOpen: true, initiatedByUserAction: true }); - renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); + findCloseButtonByActiveDrawerId('global-drawer', renderProps)!.click(); expect(onToggle).toHaveBeenCalledWith({ isOpen: false, initiatedByUserAction: true }); }); }); @@ -1426,6 +1438,26 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); } }; + const findExpandedModeButtonByActiveDrawerId = ( + id: string, + renderProps: Awaited> + ) => { + if (type === 'global') { + return renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(id); + } + + return createWrapper(renderProps.container).findButtonGroup()!.findButtonById('expand'); + }; + const findCloseButtonByActiveDrawerId = ( + id: string, + renderProps: Awaited> + ) => { + if (type === 'global') { + return renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId(id); + } else { + return createWrapper(renderProps.container).findButtonGroup()!.findButtonById('close'); + } + }; test('should set a drawer to expanded mode by clicking on "expanded mode" button', async () => { const drawerId = 'global-drawer'; @@ -1441,39 +1473,42 @@ describe('toolbar mode only features', () => { const { globalDrawersWrapper } = renderProps; findDrawerTriggerById(drawerId, renderProps)!.click(); - expect( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() - ).toBeInTheDocument(); + expect(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()).toBeInTheDocument(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); - expect( - getGeneratedAnalyticsMetadata( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() - ) - ).toEqual( - expect.objectContaining({ - action: 'expand', - detail: { - label: 'Expanded mode button', - }, - }) - ); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + if (type === 'global') { + expect( + getGeneratedAnalyticsMetadata(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()) + ).toEqual( + expect.objectContaining({ + action: 'expand', + detail: { + label: 'Expanded mode button', + }, + }) + ); + } else { + // TODO + } + + findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); - expect( - getGeneratedAnalyticsMetadata( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() - ) - ).toEqual( - expect.objectContaining({ - action: 'collapse', - detail: { - label: 'Expanded mode button', - }, - }) - ); + if (type === 'global') { + expect( + getGeneratedAnalyticsMetadata(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()) + ).toEqual( + expect.objectContaining({ + action: 'collapse', + detail: { + label: 'Expanded mode button', + }, + }) + ); + } else { + // TODO + } }); test('only one drawer could be in expanded mode. all other panels should be closed', async () => { @@ -1545,10 +1580,10 @@ describe('toolbar mode only features', () => { await delay(); findDrawerTriggerById(drawerId, renderProps)!.click(); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(true); - globalDrawersWrapper.findCloseButtonByActiveDrawerId(drawerId)!.click(); + findCloseButtonByActiveDrawerId(drawerId, renderProps)!.click(); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(false); }); }); diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx index 1023681daf..1ff5548ebe 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx @@ -92,12 +92,6 @@ export function AppLayoutGlobalAiDrawerImplementation({ // resized in either direction, so we disable the resize handler const isResizingDisabled = maxAiDrawerSize < activeAiDrawerSize; let drawerActions: ReadonlyArray = [ - { - type: 'icon-button', - id: 'expand', - iconName: isExpanded ? 'shrink' : 'expand', - text: activeAiDrawer?.ariaLabels?.expandedModeButton ?? '', - }, { type: 'icon-button', id: 'close', @@ -105,11 +99,22 @@ export function AppLayoutGlobalAiDrawerImplementation({ text: computedAriaLabels.closeButton, }, ]; + if (!isMobile && activeAiDrawer?.isExpandable) { + drawerActions = [ + { + type: 'icon-button', + id: 'expand', + iconName: isExpanded ? 'shrink' : 'expand', + text: activeAiDrawer?.ariaLabels?.expandedModeButton ?? '', + }, + ...drawerActions, + ]; + } if (activeAiDrawer?.headerActions) { drawerActions = [ { type: 'group', - text: 'Vote', + text: 'Actions', items: activeAiDrawer.headerActions!, }, ...drawerActions, @@ -192,37 +197,9 @@ export function AppLayoutGlobalAiDrawerImplementation({ activeAiDrawer?.onHeaderActionClick?.(event); } }} - ariaLabel="Chat actions" + ariaLabel="Left panel actions" items={drawerActions} /> - {/*{!isMobile && activeAiDrawer?.isExpandable && (*/} - {/*
*/} - {/* setExpandedDrawerId(isExpanded ? null : activeDrawerId!)}*/} - {/* variant="icon"*/} - {/* analyticsAction={isExpanded ? 'expand' : 'collapse'}*/} - {/* />*/} - {/*
*/} - {/*)}*/} - {/*
*/} - {/* onActiveAiDrawerChange?.(null, { initiatedByUserAction: true })}*/} - {/* ref={aiDrawerFocusControl?.refs.close}*/} - {/* variant="icon"*/} - {/* analyticsAction="close"*/} - {/* />*/} - {/*
*/}
{!isMobile && isExpanded && activeAiDrawer?.ariaLabels?.exitExpandedModeButton && ( From f7af00b34295bd1e267b8b517c0eec5d63134629 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 2 Sep 2025 10:51:44 +0200 Subject: [PATCH 03/12] chore: Increase limit size --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42fbea0124..971f6f8eb7 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "887 kB", + "limit": "888 kB", "ignore": "react-dom" } ], From 79b3d05cc2c79eef1d99aa49ea626a84a8c73516 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 2 Sep 2025 13:21:10 +0200 Subject: [PATCH 04/12] chore: U test --- .../__tests__/runtime-drawers.test.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index 5463b68650..b84ad11c56 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -957,6 +957,9 @@ describe('toolbar mode only features', () => { return createWrapper(renderProps.container).findButtonGroup()!.findButtonById('close'); } }; + const findLeftDrawerHeaderActionById = (id: string, renderProps: Awaited>) => { + return createWrapper(renderProps.container).findButtonGroup()!.findButtonById(id); + }; test('renders resize handle for a global drawer when config is enabled', async () => { registerDrawer({ @@ -1198,6 +1201,31 @@ describe('toolbar mode only features', () => { findCloseButtonByActiveDrawerId('global-drawer', renderProps)!.click(); expect(onToggle).toHaveBeenCalledWith({ isOpen: false, initiatedByUserAction: true }); }); + + (type === 'global-ai' ? test : test.skip)( + `calls onHeaderActionClick handler by clicking on drawers header action button in left runtime drawer)`, + async () => { + const onHeaderActionClick = jest.fn(); + awsuiWidgetPlugins.registerLeftDrawer({ + ...drawerDefaults, + id: 'global-drawer', + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: event => onHeaderActionClick(event.detail), + }); + const renderProps = await renderComponent(); + findDrawerTriggerById('global-drawer', renderProps)!.click(); + + findLeftDrawerHeaderActionById('add', renderProps)!.click(); + expect(onHeaderActionClick).toHaveBeenCalledWith({ id: 'add' }); + } + ); }); test('the order of the opened global drawers should match the positions of their corresponding toggle buttons on the toolbar', async () => { From e547dbb885c5f7861eb6b6a01e1a61852eef16c3 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 2 Sep 2025 15:48:57 +0200 Subject: [PATCH 05/12] chore: Make button group send analyticsAction --- .../__tests__/runtime-drawers.test.tsx | 48 ++++++++----------- .../drawer/global-ai-drawer.tsx | 4 +- src/button-group/icon-button-item.tsx | 3 +- src/button-group/interfaces.ts | 7 +++ 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index b84ad11c56..2a726006e4 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -1506,37 +1506,29 @@ describe('toolbar mode only features', () => { findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); - if (type === 'global') { - expect( - getGeneratedAnalyticsMetadata(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()) - ).toEqual( - expect.objectContaining({ - action: 'expand', - detail: { - label: 'Expanded mode button', - }, - }) - ); - } else { - // TODO - } + expect( + getGeneratedAnalyticsMetadata(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()) + ).toEqual( + expect.objectContaining({ + action: 'expand', + detail: expect.objectContaining({ + label: 'Expanded mode button', + }), + }) + ); findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); - if (type === 'global') { - expect( - getGeneratedAnalyticsMetadata(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()) - ).toEqual( - expect.objectContaining({ - action: 'collapse', - detail: { - label: 'Expanded mode button', - }, - }) - ); - } else { - // TODO - } + expect( + getGeneratedAnalyticsMetadata(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()) + ).toEqual( + expect.objectContaining({ + action: 'collapse', + detail: expect.objectContaining({ + label: 'Expanded mode button', + }), + }) + ); }); test('only one drawer could be in expanded mode. all other panels should be closed', async () => { diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx index 1ff5548ebe..676865f0f5 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx @@ -91,12 +91,13 @@ export function AppLayoutGlobalAiDrawerImplementation({ // (window is between mobile and desktop sizes). At this point, the drawer can't be // resized in either direction, so we disable the resize handler const isResizingDisabled = maxAiDrawerSize < activeAiDrawerSize; - let drawerActions: ReadonlyArray = [ + let drawerActions: ReadonlyArray = [ { type: 'icon-button', id: 'close', iconName: isMobile ? 'close' : 'angle-left', text: computedAriaLabels.closeButton, + analyticsAction: 'close', }, ]; if (!isMobile && activeAiDrawer?.isExpandable) { @@ -106,6 +107,7 @@ export function AppLayoutGlobalAiDrawerImplementation({ id: 'expand', iconName: isExpanded ? 'shrink' : 'expand', text: activeAiDrawer?.ariaLabels?.expandedModeButton ?? '', + analyticsAction: isExpanded ? 'expand' : 'collapse', }, ...drawerActions, ]; diff --git a/src/button-group/icon-button-item.tsx b/src/button-group/icon-button-item.tsx index 9b7040b5da..946a079127 100644 --- a/src/button-group/icon-button-item.tsx +++ b/src/button-group/icon-button-item.tsx @@ -15,7 +15,7 @@ import { ButtonGroupProps } from './interfaces.js'; import testUtilStyles from './test-classes/styles.css.js'; interface IconButtonItemProps { - item: ButtonGroupProps.IconButton; + item: ButtonGroupProps.InternalIconButton; showTooltip: boolean; showFeedback: boolean; onTooltipDismiss: () => void; @@ -55,6 +55,7 @@ const IconButtonItem = forwardRef( data-testid={item.id} data-itemid={item.id} className={clsx(testUtilStyles.item, testUtilStyles['button-group-item'])} + analyticsAction={item.analyticsAction} __title="" > {item.text} diff --git a/src/button-group/interfaces.ts b/src/button-group/interfaces.ts index 02c327a689..e0515a97ae 100644 --- a/src/button-group/interfaces.ts +++ b/src/button-group/interfaces.ts @@ -109,13 +109,16 @@ export interface InternalButtonGroupProps extends SomeRequired, InternalBaseComponentProps { style?: ButtonGroupProps.Style; + items: ReadonlyArray; } export namespace ButtonGroupProps { export type Variant = 'icon'; export type ItemOrGroup = Item | Group; + export type InternalItemOrGroup = InternalItem | Group; export type Item = IconButton | IconToggleButton | IconFileInput | MenuDropdown; + export type InternalItem = InternalIconButton | IconToggleButton | IconFileInput | MenuDropdown; export interface IconButton { type: 'icon-button'; @@ -132,6 +135,10 @@ export namespace ButtonGroupProps { popoverFeedback?: React.ReactNode; } + export interface InternalIconButton extends IconButton { + analyticsAction?: string; + } + export interface IconToggleButton { type: 'icon-toggle-button'; id: string; From 79f46c2b120109f7d16565d5d3606fc12c0546a0 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 3 Sep 2025 18:00:22 +0200 Subject: [PATCH 06/12] chore: Implement headerActions api for global drawers --- package.json | 2 +- pages/app-layout/utils/external-widget.tsx | 34 ++++++-- .../__tests__/runtime-drawers.test.tsx | 61 ++++--------- src/app-layout/__tests__/utils.tsx | 29 +++++-- .../drawer/global-drawer.tsx | 87 ++++++++++++------- src/internal/plugins/controllers/drawers.ts | 3 + 6 files changed, 127 insertions(+), 89 deletions(-) diff --git a/package.json b/package.json index 971f6f8eb7..e7fbca2af3 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "888 kB", + "limit": "889 kB", "ignore": "react-dom" } ], diff --git a/pages/app-layout/utils/external-widget.tsx b/pages/app-layout/utils/external-widget.tsx index 1fd2d3fa2b..d9975f02a1 100644 --- a/pages/app-layout/utils/external-widget.tsx +++ b/pages/app-layout/utils/external-widget.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; +import { StatusIndicator } from '~components'; import Box from '~components/box'; -import ButtonDropdown from '~components/button-dropdown'; import Drawer from '~components/drawer'; import awsuiPlugins from '~components/internal/plugins'; @@ -67,6 +67,18 @@ awsuiPlugins.appLayout.registerDrawer({ ReactDOM.render(, container); }, unmountContent: container => unmountComponentAtNode(container), + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + popoverFeedback: Message copied, + }, + ], + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); awsuiPlugins.appLayout.registerDrawer({ @@ -140,6 +152,7 @@ awsuiPlugins.appLayout.registerDrawer({ content: 'Content', triggerButton: 'Trigger button', resizeHandle: 'Resize handle', + expandedModeButton: 'Expanded mode button', }, onToggle: event => { console.log('circle-global drawer on toggle', event.detail); @@ -158,12 +171,7 @@ awsuiPlugins.appLayout.registerDrawer({ mountContent: (container, mountContext) => { ReactDOM.render( - Global drawer} - headerActions={ - - } - > + Global drawer}> global widget content circle 1 {new Array(100).fill(null).map((_, index) => ( @@ -176,6 +184,18 @@ awsuiPlugins.appLayout.registerDrawer({ ); }, unmountContent: container => unmountComponentAtNode(container), + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + popoverFeedback: Message copied, + }, + ], + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); awsuiPlugins.appLayout.registerDrawer({ diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index 2a726006e4..029e185894 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -947,16 +947,6 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); } }; - const findCloseButtonByActiveDrawerId = ( - id: string, - renderProps: Awaited> - ) => { - if (type === 'global') { - return renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId(id); - } else { - return createWrapper(renderProps.container).findButtonGroup()!.findButtonById('close'); - } - }; const findLeftDrawerHeaderActionById = (id: string, renderProps: Awaited>) => { return createWrapper(renderProps.container).findButtonGroup()!.findButtonById(id); }; @@ -1004,8 +994,7 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer')!.getElement()).toBeInTheDocument(); - renderProps.debug(findCloseButtonByActiveDrawerId('global-drawer', renderProps)!.getElement()); - findCloseButtonByActiveDrawerId('global-drawer', renderProps)!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer')).toBeNull(); }); @@ -1089,7 +1078,7 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer-1', renderProps)!.focus(); findDrawerTriggerById('global-drawer-1', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); - findCloseButtonByActiveDrawerId('global-drawer-1', renderProps)!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')).toBeNull(); await waitFor(() => { expect(findDrawerTriggerById('global-drawer-1', renderProps)!.getElement()).toHaveFocus(); @@ -1114,7 +1103,7 @@ describe('toolbar mode only features', () => { await delay(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); - findCloseButtonByActiveDrawerId('global-drawer-1', renderProps)!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(false); @@ -1144,7 +1133,7 @@ describe('toolbar mode only features', () => { expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); expect(onVisibilityChangeMock).toHaveBeenCalledWith(true); - findCloseButtonByActiveDrawerId('global-drawer-1', renderProps)!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); expect(onVisibilityChangeMock).toHaveBeenCalledWith(false); }); @@ -1198,7 +1187,7 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer', renderProps)!.click(); expect(onToggle).toHaveBeenCalledWith({ isOpen: true, initiatedByUserAction: true }); - findCloseButtonByActiveDrawerId('global-drawer', renderProps)!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); expect(onToggle).toHaveBeenCalledWith({ isOpen: false, initiatedByUserAction: true }); }); @@ -1466,26 +1455,6 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); } }; - const findExpandedModeButtonByActiveDrawerId = ( - id: string, - renderProps: Awaited> - ) => { - if (type === 'global') { - return renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(id); - } - - return createWrapper(renderProps.container).findButtonGroup()!.findButtonById('expand'); - }; - const findCloseButtonByActiveDrawerId = ( - id: string, - renderProps: Awaited> - ) => { - if (type === 'global') { - return renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId(id); - } else { - return createWrapper(renderProps.container).findButtonGroup()!.findButtonById('close'); - } - }; test('should set a drawer to expanded mode by clicking on "expanded mode" button', async () => { const drawerId = 'global-drawer'; @@ -1501,13 +1470,17 @@ describe('toolbar mode only features', () => { const { globalDrawersWrapper } = renderProps; findDrawerTriggerById(drawerId, renderProps)!.click(); - expect(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()).toBeInTheDocument(); + expect( + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + ).toBeInTheDocument(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); - findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); expect( - getGeneratedAnalyticsMetadata(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()) + getGeneratedAnalyticsMetadata( + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + ) ).toEqual( expect.objectContaining({ action: 'expand', @@ -1517,10 +1490,12 @@ describe('toolbar mode only features', () => { }) ); - findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); expect( - getGeneratedAnalyticsMetadata(findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.getElement()) + getGeneratedAnalyticsMetadata( + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + ) ).toEqual( expect.objectContaining({ action: 'collapse', @@ -1600,10 +1575,10 @@ describe('toolbar mode only features', () => { await delay(); findDrawerTriggerById(drawerId, renderProps)!.click(); - findExpandedModeButtonByActiveDrawerId(drawerId, renderProps)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(true); - findCloseButtonByActiveDrawerId(drawerId, renderProps)!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(false); }); }); diff --git a/src/app-layout/__tests__/utils.tsx b/src/app-layout/__tests__/utils.tsx index a52b6a6da7..9e8eb95fb2 100644 --- a/src/app-layout/__tests__/utils.tsx +++ b/src/app-layout/__tests__/utils.tsx @@ -12,7 +12,12 @@ import AppLayout, { AppLayoutProps } from '../../../lib/components/app-layout'; import customCssProps from '../../../lib/components/internal/generated/custom-css-properties'; import { forceMobileModeSymbol } from '../../../lib/components/internal/hooks/use-mobile'; import { SplitPanelProps } from '../../../lib/components/split-panel'; -import createWrapper, { AppLayoutWrapper, ElementWrapper } from '../../../lib/components/test-utils/dom'; +import createWrapper, { + AppLayoutWrapper, + ButtonGroupWrapper, + ButtonWrapper, + ElementWrapper, +} from '../../../lib/components/test-utils/dom'; import testutilStyles from '../../../lib/components/app-layout/test-classes/styles.css.js'; import visualRefreshStyles from '../../../lib/components/app-layout/visual-refresh/styles.css.js'; @@ -206,16 +211,22 @@ export const getGlobalDrawersTestUtils = (wrapper: AppLayoutWrapper) => { ); }, - findCloseButtonByActiveDrawerId(id: string): ElementWrapper | null { - return wrapper.find( - `.${testutilStyles['active-drawer']}[data-testid="awsui-app-layout-drawer-${id}"] .${testutilStyles['active-drawer-close-button']}` - ); + findCloseButtonByActiveDrawerId(id: string): ButtonWrapper | null { + return wrapper + .findComponent( + `.${testutilStyles['active-drawer']}[data-testid="awsui-app-layout-drawer-${id}"]`, + ButtonGroupWrapper + )! + .findButtonById('close'); }, - findExpandedModeButtonByActiveDrawerId(id: string): ElementWrapper | null { - return wrapper.find( - `.${testutilStyles['active-drawer']}[data-testid="awsui-app-layout-drawer-${id}"] .${testutilStyles['active-drawer-expanded-mode-button']}` - ); + findExpandedModeButtonByActiveDrawerId(id: string): ButtonWrapper | null { + return wrapper + .findComponent( + `.${testutilStyles['active-drawer']}[data-testid="awsui-app-layout-drawer-${id}"]`, + ButtonGroupWrapper + )! + .findButtonById('expand'); }, findLeaveExpandedModeButtonInAIDrawer(): ElementWrapper | null { diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx index 3acfa57ae4..0cc68b3969 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx @@ -4,11 +4,13 @@ import React, { useRef } from 'react'; import { Transition } from 'react-transition-group'; import clsx from 'clsx'; -import { InternalButton } from '../../../button/internal'; +import { ButtonGroupProps } from '../../../button-group/interfaces'; +import ButtonGroup from '../../../button-group/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; import customCssProps from '../../../internal/generated/custom-css-properties'; import { usePrevious } from '../../../internal/hooks/use-previous'; import { getLimitedValue } from '../../../split-panel/utils/size-utils'; +import { Focusable } from '../../utils/use-focus-control'; import { getDrawerStyles } from '../compute-layout'; import { AppLayoutInternals, InternalDrawer } from '../interfaces'; import { useResize } from './use-resize'; @@ -74,6 +76,37 @@ function AppLayoutGlobalDrawerImplementation({ const animationDisabled = (activeGlobalDrawer?.defaultActive && !drawersOpenQueue.includes(activeGlobalDrawer.id)) || (wasExpanded && !isExpanded); + let drawerActions: ReadonlyArray = [ + { + type: 'icon-button', + id: 'close', + iconName: isMobile ? 'close' : 'angle-right', + text: computedAriaLabels.closeButton ?? '', + analyticsAction: 'close', + }, + ]; + if (!isMobile && activeGlobalDrawer?.isExpandable) { + drawerActions = [ + { + type: 'icon-button', + id: 'expand', + iconName: isExpanded ? 'shrink' : 'expand', + text: activeGlobalDrawer?.ariaLabels?.expandedModeButton ?? '', + analyticsAction: isExpanded ? 'expand' : 'collapse', + }, + ...drawerActions, + ]; + } + if (activeGlobalDrawer?.headerActions) { + drawerActions = [ + { + type: 'group', + text: 'Actions', + items: activeGlobalDrawer.headerActions!, + }, + ...drawerActions, + ]; + } return ( @@ -146,34 +179,30 @@ function AppLayoutGlobalDrawerImplementation({ data-testid={`awsui-app-layout-drawer-content-${activeDrawerId}`} >
- {!isMobile && activeGlobalDrawer?.isExpandable && ( -
- setExpandedDrawerId(isExpanded ? null : activeDrawerId)} - variant="icon" - analyticsAction={isExpanded ? 'expand' : 'collapse'} - /> -
- )} -
- onActiveGlobalDrawersChange(activeDrawerId, { initiatedByUserAction: true })} - ref={refs?.close} - variant="icon" - analyticsAction="close" - /> -
+ { + switch (event.detail.id) { + case 'close': + onActiveGlobalDrawersChange(activeDrawerId, { initiatedByUserAction: true }); + break; + case 'expand': + setExpandedDrawerId(isExpanded ? null : activeDrawerId); + break; + default: + activeAiDrawer?.onHeaderActionClick?.(event); + } + }} + ariaLabel="Global panel actions" + items={drawerActions} + __internalRootRef={(root: HTMLElement) => { + if (!root) { + return; + } + refs.close = { current: root.querySelector('[data-itemid="close"]') as unknown as Focusable }; + }} + />
{activeGlobalDrawer?.content} diff --git a/src/internal/plugins/controllers/drawers.ts b/src/internal/plugins/controllers/drawers.ts index ecc9d3058f..87d4c20473 100644 --- a/src/internal/plugins/controllers/drawers.ts +++ b/src/internal/plugins/controllers/drawers.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { ButtonGroupProps } from '../../../button-group/interfaces'; import debounce from '../../debounce'; import { NonCancelableEventHandler } from '../../events'; import { reportRuntimeApiWarning } from '../helpers/metrics'; @@ -40,6 +41,8 @@ export interface DrawerConfig { unmountContent: (container: HTMLElement) => void; preserveInactiveContent?: boolean; onToggle?: NonCancelableEventHandler; + headerActions?: ReadonlyArray; + onHeaderActionClick?: NonCancelableEventHandler; } const updatableProperties = [ From 3fd304770a256e2fd8256ff6aea86512efbc3e78 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 4 Sep 2025 11:17:12 +0200 Subject: [PATCH 07/12] fix: Global drawer headerActions --- .../__tests__/runtime-drawers.test.tsx | 45 +++++++++---------- .../drawer/global-drawer.tsx | 2 +- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index 029e185894..45ddfdbb06 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -947,7 +947,7 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); } }; - const findLeftDrawerHeaderActionById = (id: string, renderProps: Awaited>) => { + const findDrawerHeaderActionById = (id: string, renderProps: Awaited>) => { return createWrapper(renderProps.container).findButtonGroup()!.findButtonById(id); }; @@ -1191,30 +1191,27 @@ describe('toolbar mode only features', () => { expect(onToggle).toHaveBeenCalledWith({ isOpen: false, initiatedByUserAction: true }); }); - (type === 'global-ai' ? test : test.skip)( - `calls onHeaderActionClick handler by clicking on drawers header action button in left runtime drawer)`, - async () => { - const onHeaderActionClick = jest.fn(); - awsuiWidgetPlugins.registerLeftDrawer({ - ...drawerDefaults, - id: 'global-drawer', - headerActions: [ - { - type: 'icon-button', - id: 'add', - iconName: 'add-plus', - text: 'Add', - }, - ], - onHeaderActionClick: event => onHeaderActionClick(event.detail), - }); - const renderProps = await renderComponent(); - findDrawerTriggerById('global-drawer', renderProps)!.click(); + test(`calls onHeaderActionClick handler by clicking on drawers header action button in left runtime drawer)`, async () => { + const onHeaderActionClick = jest.fn(); + registerDrawer({ + ...drawerDefaults, + id: 'global-drawer', + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: event => onHeaderActionClick(event.detail), + }); + const renderProps = await renderComponent(); + findDrawerTriggerById('global-drawer', renderProps)!.click(); - findLeftDrawerHeaderActionById('add', renderProps)!.click(); - expect(onHeaderActionClick).toHaveBeenCalledWith({ id: 'add' }); - } - ); + findDrawerHeaderActionById('add', renderProps)!.click(); + expect(onHeaderActionClick).toHaveBeenCalledWith({ id: 'add' }); + }); }); test('the order of the opened global drawers should match the positions of their corresponding toggle buttons on the toolbar', async () => { diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx index 0cc68b3969..1f185e3233 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx @@ -191,7 +191,7 @@ function AppLayoutGlobalDrawerImplementation({ setExpandedDrawerId(isExpanded ? null : activeDrawerId); break; default: - activeAiDrawer?.onHeaderActionClick?.(event); + activeGlobalDrawer?.onHeaderActionClick?.(event); } }} ariaLabel="Global panel actions" From 6c0598ab757b8f159770c386a840878fc915377a Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 4 Sep 2025 16:47:51 +0200 Subject: [PATCH 08/12] chore: Increase size limit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7fbca2af3..cbff2ebddf 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "889 kB", + "limit": "890 kB", "ignore": "react-dom" } ], From 1e43c48c611207881c6033dc80e48805eca2cad1 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 4 Sep 2025 17:29:40 +0200 Subject: [PATCH 09/12] fix: Drawer's expanded button selector for an integ test --- src/app-layout/__integ__/runtime-drawers.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app-layout/__integ__/runtime-drawers.test.ts b/src/app-layout/__integ__/runtime-drawers.test.ts index e8becfdabc..f2597a21dc 100644 --- a/src/app-layout/__integ__/runtime-drawers.test.ts +++ b/src/app-layout/__integ__/runtime-drawers.test.ts @@ -3,12 +3,11 @@ import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; -import createWrapper, { AppLayoutWrapper } from '../../../lib/components/test-utils/selectors'; +import createWrapper, { AppLayoutWrapper, ButtonGroupWrapper } from '../../../lib/components/test-utils/selectors'; import { Theme } from '../../__integ__/utils.js'; import { viewports } from './constants'; import { getUrlParams } from './utils'; -import testUtilsStyles from '../../../lib/components/app-layout/test-classes/styles.selectors.js'; import vrDrawerStyles from '../../../lib/components/app-layout/visual-refresh/styles.selectors.js'; import vrToolbarDrawerStyles from '../../../lib/components/app-layout/visual-refresh-toolbar/drawer/styles.selectors.js'; @@ -21,9 +20,9 @@ const findDrawerContentById = (wrapper: AppLayoutWrapper, id: string) => { }; const findExpandedModeButtonByActiveDrawerId = (wrapper: AppLayoutWrapper, id: string) => { - return wrapper.find( - `[data-testid="awsui-app-layout-drawer-${id}"] .${testUtilsStyles['active-drawer-expanded-mode-button']}` - ); + return wrapper + .findComponent(`[data-testid="awsui-app-layout-drawer-${id}"]`, ButtonGroupWrapper)! + .findButtonById('expand'); }; describe.each(['classic', 'refresh', 'refresh-toolbar'] as Theme[])('%s', theme => { From 31ce8f7b855b4603a46f670c3eac814085ef869f Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Fri, 5 Sep 2025 11:20:35 +0200 Subject: [PATCH 10/12] chore: Bring InternalIconButton out of the public ButtonGroupProps namespace --- .../drawer/global-ai-drawer.tsx | 4 ++-- .../drawer/global-drawer.tsx | 4 ++-- src/button-group/icon-button-item.tsx | 4 ++-- src/button-group/interfaces.ts | 19 ++++++++++++------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx index 676865f0f5..fc4ff30fba 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx @@ -4,7 +4,7 @@ import React, { useRef } from 'react'; import { Transition } from 'react-transition-group'; import clsx from 'clsx'; -import { ButtonGroupProps } from '../../../button-group/interfaces'; +import { InternalItemOrGroup } from '../../../button-group/interfaces'; import ButtonGroup from '../../../button-group/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; import customCssProps from '../../../internal/generated/custom-css-properties'; @@ -91,7 +91,7 @@ export function AppLayoutGlobalAiDrawerImplementation({ // (window is between mobile and desktop sizes). At this point, the drawer can't be // resized in either direction, so we disable the resize handler const isResizingDisabled = maxAiDrawerSize < activeAiDrawerSize; - let drawerActions: ReadonlyArray = [ + let drawerActions: ReadonlyArray = [ { type: 'icon-button', id: 'close', diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx index 1f185e3233..27ed111208 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx @@ -4,7 +4,7 @@ import React, { useRef } from 'react'; import { Transition } from 'react-transition-group'; import clsx from 'clsx'; -import { ButtonGroupProps } from '../../../button-group/interfaces'; +import { InternalItemOrGroup } from '../../../button-group/interfaces'; import ButtonGroup from '../../../button-group/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; import customCssProps from '../../../internal/generated/custom-css-properties'; @@ -76,7 +76,7 @@ function AppLayoutGlobalDrawerImplementation({ const animationDisabled = (activeGlobalDrawer?.defaultActive && !drawersOpenQueue.includes(activeGlobalDrawer.id)) || (wasExpanded && !isExpanded); - let drawerActions: ReadonlyArray = [ + let drawerActions: ReadonlyArray = [ { type: 'icon-button', id: 'close', diff --git a/src/button-group/icon-button-item.tsx b/src/button-group/icon-button-item.tsx index 946a079127..4a2861f141 100644 --- a/src/button-group/icon-button-item.tsx +++ b/src/button-group/icon-button-item.tsx @@ -10,12 +10,12 @@ import { InternalButton } from '../button/internal.js'; import Tooltip from '../internal/components/tooltip/index.js'; import { CancelableEventHandler, fireCancelableEvent } from '../internal/events/index.js'; import InternalLiveRegion from '../live-region/internal.js'; -import { ButtonGroupProps } from './interfaces.js'; +import { ButtonGroupProps, InternalIconButton } from './interfaces.js'; import testUtilStyles from './test-classes/styles.css.js'; interface IconButtonItemProps { - item: ButtonGroupProps.InternalIconButton; + item: InternalIconButton; showTooltip: boolean; showFeedback: boolean; onTooltipDismiss: () => void; diff --git a/src/button-group/interfaces.ts b/src/button-group/interfaces.ts index e0515a97ae..00d434c8eb 100644 --- a/src/button-group/interfaces.ts +++ b/src/button-group/interfaces.ts @@ -105,20 +105,29 @@ export interface ButtonGroupProps extends BaseComponentProps { style?: ButtonGroupProps.Style; } +export interface InternalIconButton extends ButtonGroupProps.IconButton { + analyticsAction?: string; +} + +export type InternalItemOrGroup = InternalItem | ButtonGroupProps.Group; +export type InternalItem = + | InternalIconButton + | ButtonGroupProps.IconToggleButton + | ButtonGroupProps.IconFileInput + | ButtonGroupProps.MenuDropdown; + export interface InternalButtonGroupProps extends SomeRequired, InternalBaseComponentProps { style?: ButtonGroupProps.Style; - items: ReadonlyArray; + items: ReadonlyArray; } export namespace ButtonGroupProps { export type Variant = 'icon'; export type ItemOrGroup = Item | Group; - export type InternalItemOrGroup = InternalItem | Group; export type Item = IconButton | IconToggleButton | IconFileInput | MenuDropdown; - export type InternalItem = InternalIconButton | IconToggleButton | IconFileInput | MenuDropdown; export interface IconButton { type: 'icon-button'; @@ -135,10 +144,6 @@ export namespace ButtonGroupProps { popoverFeedback?: React.ReactNode; } - export interface InternalIconButton extends IconButton { - analyticsAction?: string; - } - export interface IconToggleButton { type: 'icon-toggle-button'; id: string; From cb65c0233e63ad96688c06833c2bee3d302f7f3b Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Fri, 5 Sep 2025 14:20:26 +0200 Subject: [PATCH 11/12] chore: Throw a warning if unsupported props are used inside headerActions in runtime api --- src/app-layout/runtime-drawer/index.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/app-layout/runtime-drawer/index.tsx b/src/app-layout/runtime-drawer/index.tsx index 2a40354c1c..0336991328 100644 --- a/src/app-layout/runtime-drawer/index.tsx +++ b/src/app-layout/runtime-drawer/index.tsx @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useContext, useEffect, useRef } from 'react'; +import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; + +import { ButtonGroupProps } from '../../button-group/interfaces'; import { fireNonCancelableEvent, NonCancelableEventHandler } from '../../internal/events'; import { DrawerConfig as RuntimeDrawerConfig, @@ -79,11 +82,29 @@ function RuntimeDrawerHeader({ mountHeader, unmountHeader }: RuntimeContentHeade return
; } +function checkForUnsupportedProps(headerActions: ReadonlyArray) { + const unsupportedProps = new Set([ + 'iconSvg', + 'popoverFeedback', + 'pressedIconSvg', + 'popoverFeedback', + 'pressedPopoverFeedback', + ]); + for (const item of headerActions) { + const unsupported = Object.keys(item).filter(key => unsupportedProps.has(key)); + if (unsupported.length > 0) { + warnOnce('AppLayout', `The headerActions properties are not supported for runtime api: ${unsupported.join(' ')}`); + } + } + return headerActions; +} + export const mapRuntimeConfigToDrawer = ( runtimeConfig: RuntimeDrawerConfig ): AppLayoutProps.Drawer & { orderPriority?: number; onToggle?: NonCancelableEventHandler; + headerActions?: ReadonlyArray; } => { const { mountContent, unmountContent, trigger, ...runtimeDrawer } = runtimeConfig; @@ -111,6 +132,7 @@ export const mapRuntimeConfigToDrawer = ( onResize: event => { fireNonCancelableEvent(runtimeDrawer.onResize, { size: event.detail.size, id: runtimeDrawer.id }); }, + headerActions: runtimeDrawer.headerActions ? checkForUnsupportedProps(runtimeDrawer.headerActions) : undefined, }; }; @@ -119,6 +141,7 @@ export const mapRuntimeConfigToAiDrawer = ( ): AppLayoutProps.Drawer & { orderPriority?: number; onToggle?: NonCancelableEventHandler; + headerActions?: ReadonlyArray; } => { const { mountContent, unmountContent, trigger, ...runtimeDrawer } = runtimeConfig; @@ -153,6 +176,7 @@ export const mapRuntimeConfigToAiDrawer = ( onResize: event => { fireNonCancelableEvent(runtimeDrawer.onResize, { size: event.detail.size, id: runtimeDrawer.id }); }, + headerActions: runtimeDrawer.headerActions ? checkForUnsupportedProps(runtimeDrawer.headerActions) : undefined, }; }; From 1bc402de679de9e3c132275d6dbbcf4c0b0ede66 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Fri, 5 Sep 2025 17:01:52 +0200 Subject: [PATCH 12/12] chore: Remove popoverFeedback from headerActions --- pages/app-layout/utils/external-global-left-panel-widget.tsx | 3 +-- pages/app-layout/utils/external-widget.tsx | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) 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 4757fa98e8..007fd959f0 100644 --- a/pages/app-layout/utils/external-global-left-panel-widget.tsx +++ b/pages/app-layout/utils/external-global-left-panel-widget.tsx @@ -3,7 +3,7 @@ import React from 'react'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; -import { Box, StatusIndicator } from '~components'; +import { Box } from '~components'; import { registerLeftDrawer } from '~components/internal/plugins/widget'; import styles from '../styles.scss'; @@ -94,7 +94,6 @@ registerLeftDrawer({ id: 'add', iconName: 'add-plus', text: 'Add', - popoverFeedback: Message copied, }, ], diff --git a/pages/app-layout/utils/external-widget.tsx b/pages/app-layout/utils/external-widget.tsx index d9975f02a1..d010b011e2 100644 --- a/pages/app-layout/utils/external-widget.tsx +++ b/pages/app-layout/utils/external-widget.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; -import { StatusIndicator } from '~components'; import Box from '~components/box'; import Drawer from '~components/drawer'; import awsuiPlugins from '~components/internal/plugins'; @@ -73,7 +72,6 @@ awsuiPlugins.appLayout.registerDrawer({ id: 'add', iconName: 'add-plus', text: 'Add', - popoverFeedback: Message copied, }, ], onHeaderActionClick: ({ detail }) => { @@ -190,7 +188,6 @@ awsuiPlugins.appLayout.registerDrawer({ id: 'add', iconName: 'add-plus', text: 'Add', - popoverFeedback: Message copied, }, ], onHeaderActionClick: ({ detail }) => {