diff --git a/package.json b/package.json index f491041e74..cbff2ebddf 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "869 kB", + "limit": "890 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..007fd959f0 100644 --- a/pages/app-layout/utils/external-global-left-panel-widget.tsx +++ b/pages/app-layout/utils/external-global-left-panel-widget.tsx @@ -4,8 +4,6 @@ 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 { registerLeftDrawer } from '~components/internal/plugins/widget'; import styles from '../styles.scss'; @@ -69,20 +67,37 @@ 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', + }, + ], + + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); diff --git a/pages/app-layout/utils/external-widget.tsx b/pages/app-layout/utils/external-widget.tsx index 1fd2d3fa2b..d010b011e2 100644 --- a/pages/app-layout/utils/external-widget.tsx +++ b/pages/app-layout/utils/external-widget.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import Box from '~components/box'; -import ButtonDropdown from '~components/button-dropdown'; import Drawer from '~components/drawer'; import awsuiPlugins from '~components/internal/plugins'; @@ -67,6 +66,17 @@ awsuiPlugins.appLayout.registerDrawer({ ReactDOM.render(, container); }, unmountContent: container => unmountComponentAtNode(container), + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); awsuiPlugins.appLayout.registerDrawer({ @@ -140,6 +150,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 +169,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 +182,17 @@ awsuiPlugins.appLayout.registerDrawer({ ); }, unmountContent: container => unmountComponentAtNode(container), + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); awsuiPlugins.appLayout.registerDrawer({ 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 => { 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..45ddfdbb06 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,9 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); } }; + const findDrawerHeaderActionById = (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({ @@ -990,7 +994,7 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer')!.getElement()).toBeInTheDocument(); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer')).toBeNull(); }); @@ -1074,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(); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.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(); @@ -1099,7 +1103,7 @@ describe('toolbar mode only features', () => { await delay(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.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); @@ -1129,7 +1133,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(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); expect(onVisibilityChangeMock).toHaveBeenCalledWith(false); }); @@ -1186,6 +1190,28 @@ describe('toolbar mode only features', () => { renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); expect(onToggle).toHaveBeenCalledWith({ isOpen: false, initiatedByUserAction: true }); }); + + 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(); + + 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 () => { @@ -1442,36 +1468,37 @@ describe('toolbar mode only features', () => { findDrawerTriggerById(drawerId, renderProps)!.click(); expect( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() ).toBeInTheDocument(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); + expect( getGeneratedAnalyticsMetadata( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() ) ).toEqual( expect.objectContaining({ action: 'expand', - detail: { + detail: expect.objectContaining({ label: 'Expanded mode button', - }, + }), }) ); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); expect( getGeneratedAnalyticsMetadata( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() ) ).toEqual( expect.objectContaining({ action: 'collapse', - detail: { + detail: expect.objectContaining({ label: 'Expanded mode button', - }, + }), }) ); }); @@ -1545,10 +1572,10 @@ describe('toolbar mode only features', () => { await delay(); findDrawerTriggerById(drawerId, renderProps)!.click(); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(true); - globalDrawersWrapper.findCloseButtonByActiveDrawerId(drawerId)!.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/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, }; }; 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..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,8 @@ import React, { useRef } from 'react'; import { Transition } from 'react-transition-group'; import clsx from 'clsx'; -import { InternalButton } from '../../../button/internal'; +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'; import { usePrevious } from '../../../internal/hooks/use-previous'; @@ -90,6 +91,37 @@ 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: 'close', + iconName: isMobile ? 'close' : 'angle-left', + text: computedAriaLabels.closeButton, + analyticsAction: 'close', + }, + ]; + if (!isMobile && activeAiDrawer?.isExpandable) { + drawerActions = [ + { + type: 'icon-button', + id: 'expand', + iconName: isExpanded ? 'shrink' : 'expand', + text: activeAiDrawer?.ariaLabels?.expandedModeButton ?? '', + analyticsAction: isExpanded ? 'expand' : 'collapse', + }, + ...drawerActions, + ]; + } + if (activeAiDrawer?.headerActions) { + drawerActions = [ + { + type: 'group', + text: 'Actions', + items: activeAiDrawer.headerActions!, + }, + ...drawerActions, + ]; + } return ( @@ -152,34 +184,24 @@ 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="Left panel actions" + items={drawerActions} + />
{!isMobile && isExpanded && activeAiDrawer?.ariaLabels?.exitExpandedModeButton && ( 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..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,11 +4,13 @@ import React, { useRef } from 'react'; import { Transition } from 'react-transition-group'; import clsx from 'clsx'; -import { InternalButton } from '../../../button/internal'; +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'; 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: + activeGlobalDrawer?.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/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/button-group/icon-button-item.tsx b/src/button-group/icon-button-item.tsx index 9b7040b5da..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.IconButton; + item: 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..00d434c8eb 100644 --- a/src/button-group/interfaces.ts +++ b/src/button-group/interfaces.ts @@ -105,10 +105,22 @@ 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; } export namespace ButtonGroupProps { 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 = [ 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>;