diff --git a/build-tools/utils/custom-css-properties.js b/build-tools/utils/custom-css-properties.js index bd820fc38d..96f18200b0 100644 --- a/build-tools/utils/custom-css-properties.js +++ b/build-tools/utils/custom-css-properties.js @@ -19,6 +19,7 @@ const customCssPropertiesList = [ 'defaultMinContentWidth', 'drawerSize', 'drawerMinSize', + 'bottomDrawerSize', 'footerHeight', 'headerGap', 'headerHeight', diff --git a/package.json b/package.json index c81726e708..687ba3f7cb 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "962 kB", + "limit": "972 kB", "ignore": "react-dom" } ], diff --git a/pages/app-layout/runtime-drawers.page.tsx b/pages/app-layout/runtime-drawers.page.tsx index 1da32a10e3..919a0d45d0 100644 --- a/pages/app-layout/runtime-drawers.page.tsx +++ b/pages/app-layout/runtime-drawers.page.tsx @@ -20,6 +20,7 @@ import './utils/external-widget'; import AppContext, { AppContextType } from '../app/app-context'; import { Breadcrumbs, Containers, CustomDrawerContent } from './utils/content-blocks'; import { drawerLabels } from './utils/drawers'; +import { registerBottomDrawer } from './utils/external-widget'; import appLayoutLabels from './utils/labels'; import { splitPaneli18nStrings } from './utils/strings'; @@ -126,6 +127,9 @@ export default function WithDrawers() { > Resize circle3-global drawer to 500px + } diff --git a/pages/app-layout/utils/external-widget.tsx b/pages/app-layout/utils/external-widget.tsx index ce8baa0cde..c110e4f23e 100644 --- a/pages/app-layout/utils/external-widget.tsx +++ b/pages/app-layout/utils/external-widget.tsx @@ -138,6 +138,7 @@ const AutoIncrementCounter: React.FC<{ awsuiPlugins.appLayout.registerDrawer({ id: 'circle-global', type: 'global', + position: 'side', defaultActive: false, resizable: true, defaultSize: 350, @@ -294,3 +295,67 @@ awsuiPlugins.appLayout.registerDrawer({ }, unmountContent: container => unmount(container), }); + +export const registerBottomDrawer = () => { + awsuiPlugins.appLayout.registerDrawer({ + id: 'circle5-global-bottom', + type: 'global', + position: 'bottom', + defaultActive: false, + resizable: true, + defaultSize: 350, + preserveInactiveContent: true, + orderPriority: 100, + + isExpandable: true, + + ariaLabels: { + closeButton: 'Close button', + content: 'Content bottom', + triggerButton: 'Trigger button', + resizeHandle: 'Resize handle', + expandedModeButton: 'Expanded mode button', + }, + onToggle: event => { + console.log('circle-global drawer on toggle', event.detail); + }, + + trigger: { + iconSvg: ` + + + `, + }, + + onResize: event => { + console.log('resize', event.detail); + }, + + mountContent: (container, mountContext) => { + mount( + Global drawer}> + + global bottom panel + {new Array(100).fill(null).map((_, index) => ( +
{index}
+ ))} +
circle-global bottom content
+
+
, + container + ); + }, + unmountContent: container => unmount(container), + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, + }); +}; diff --git a/src/app-layout/__integ__/runtime-drawers.test.ts b/src/app-layout/__integ__/runtime-drawers.test.ts index 42e105a82a..6d67aee9f3 100644 --- a/src/app-layout/__integ__/runtime-drawers.test.ts +++ b/src/app-layout/__integ__/runtime-drawers.test.ts @@ -8,6 +8,7 @@ import { Theme } from '../../__integ__/utils.js'; import { viewports } from './constants'; import { getUrlParams } from './utils'; +import testutilStyles 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'; @@ -24,6 +25,9 @@ const findExpandedModeButtonByActiveDrawerId = (wrapper: AppLayoutWrapper, id: s .findComponent(`[data-testid="awsui-app-layout-drawer-${id}"]`, ButtonGroupWrapper)! .findButtonById('expand'); }; +const findResizeHandlerByDrawerId = (wrapper: AppLayoutWrapper, id: string) => { + return wrapper.find(`[data-testid="awsui-app-layout-drawer-${id}"] .${testutilStyles['drawers-slider']}`); +}; describe.each(['classic', 'refresh', 'refresh-toolbar'] as Theme[])('%s', theme => { function setupTest( @@ -144,6 +148,10 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as Theme[])('%s', theme }); }); +interface SetupTestOptions { + splitPanelPosition?: string; +} + describe('Visual refresh toolbar only', () => { class PageObject extends BasePageObject { hasHorizontalScroll() { @@ -156,7 +164,7 @@ describe('Visual refresh toolbar only', () => { return width; } } - function setupTest(testFn: (page: PageObject) => Promise) { + function setupTest({ splitPanelPosition = 'side' }: SetupTestOptions, testFn: (page: PageObject) => Promise) { return useBrowser(async browser => { const page = new PageObject(browser); @@ -164,7 +172,7 @@ describe('Visual refresh toolbar only', () => { `#/light/app-layout/runtime-drawers?${getUrlParams('refresh-toolbar', { hasDrawers: 'false', hasTools: 'true', - splitPanelPosition: 'side', + splitPanelPosition, })}` ); await page.waitForVisible(wrapper.findDrawerTriggerById('security').toSelector(), true); @@ -174,7 +182,7 @@ describe('Visual refresh toolbar only', () => { test( 'displays only the most recently opened drawer in a full-width popup on mobile view (global drawer on top of the local one)', - setupTest(async page => { + setupTest({}, async page => { await page.click(wrapper.findDrawerTriggerById('security').toSelector()); await page.click(wrapper.findDrawerTriggerById('circle-global').toSelector()); @@ -187,7 +195,7 @@ describe('Visual refresh toolbar only', () => { test( 'displays only the most recently opened drawer in a full-width popup on mobile view (local drawer on top of the global one)', - setupTest(async page => { + setupTest({}, async page => { await page.click(wrapper.findDrawerTriggerById('circle-global').toSelector()); await page.click(wrapper.findDrawerTriggerById('security').toSelector()); @@ -200,7 +208,7 @@ describe('Visual refresh toolbar only', () => { test( 'should open 3 drawers (1 local and 2 global) if the screen size permits', - setupTest(async page => { + setupTest({}, async page => { await page.setWindowSize({ ...viewports.desktop, width: 1700 }); await page.click(wrapper.findDrawerTriggerById('security').toSelector()); await page.click(wrapper.findDrawerTriggerById('circle-global').toSelector()); @@ -217,7 +225,7 @@ describe('Visual refresh toolbar only', () => { describe('active drawers take up all available space on the page and a third drawer is opened', () => { test( 'active drawers can be shrunk to accommodate a third drawer', - setupTest(async page => { + setupTest({}, async page => { await page.setWindowSize(viewports.desktopWide); await page.click(wrapper.findDrawerTriggerById('circle-global').toSelector()); await page.click(wrapper.findDrawerTriggerById('global-with-stored-state').toSelector()); @@ -237,7 +245,7 @@ describe('Visual refresh toolbar only', () => { test( 'first opened drawer should be closed when active drawers can not be shrunk to accommodate it', - setupTest(async page => { + setupTest({}, async page => { // Give the toolbar enough horizontal space to make sure the triggers are not collapsed into a dropdown await page.setWindowSize({ ...viewports.desktop, width: 1400 }); await page.click(wrapper.findDrawerTriggerById('circle').toSelector()); @@ -257,7 +265,7 @@ describe('Visual refresh toolbar only', () => { test( 'should prevent the horizontal page scroll from appearing during resize', - setupTest(async page => { + setupTest({}, async page => { await page.setWindowSize(viewports.desktopWide); await page.click(wrapper.findDrawerTriggerById('circle').toSelector()); await page.click(wrapper.findDrawerTriggerById('global-with-stored-state').toSelector()); @@ -296,7 +304,7 @@ describe('Visual refresh toolbar only', () => { for (const viewport of ['mobile', 'desktop']) { test( `the content inside drawers should be scrollable on ${viewport} view`, - setupTest(async page => { + setupTest({}, async page => { await page.click(wrapper.findDrawerTriggerById('circle-global').toSelector()); if (viewport === 'mobile') { await page.setWindowSize(viewports.mobile); @@ -314,7 +322,7 @@ describe('Visual refresh toolbar only', () => { test( 'should show sticky elements on scroll in custom global drawer', - setupTest(async page => { + setupTest({}, async page => { await page.setWindowSize(viewports.desktop); await expect(page.isDisplayed('[data-testid="drawer-sticky-footer"]')).resolves.toBe(false); @@ -334,7 +342,7 @@ describe('Visual refresh toolbar only', () => { test( 'should show only expanded drawer and hide all other panels if expanded mode for a drawer is active', - setupTest(async page => { + setupTest({}, async page => { await page.setWindowSize(viewports.desktopWide); await page.click(wrapper.findDrawerTriggerById('circle-global').toSelector()); await page.click(wrapper.findDrawerTriggerById('circle3-global').toSelector()); @@ -356,7 +364,7 @@ describe('Visual refresh toolbar only', () => { test( 'should programmatically resize drawers', - setupTest(async page => { + setupTest({}, async page => { await page.setWindowSize(viewports.desktopWide); const drawerId1 = 'circle-global'; const drawerId2 = 'circle3-global'; @@ -381,4 +389,45 @@ describe('Visual refresh toolbar only', () => { }); }) ); + + test( + 'bottom drawer should not overlap with toolbar when resized to its max height', + setupTest({ splitPanelPosition: 'bottom' }, async page => { + await page.setWindowSize(viewports.desktopWide); + + await page.click(createWrapper().findButton('[data-testid="button-register-bottom-drawer"]').toSelector()); + + const drawerId = 'circle5-global-bottom'; + await page.click(wrapper.findDrawerTriggerById(drawerId).toSelector()); + + await expect(page.isClickable(findDrawerById(wrapper, drawerId)!.toSelector())).resolves.toBe(true); + const { top: toolbarTop, bottom: toolbarBottom } = await page.getBoundingBox( + wrapper.findToolsToggle().toSelector() + ); + const { top: drawerTopInitial } = await page.getBoundingBox(findDrawerById(wrapper, drawerId)!.toSelector()); + // resize bottom drawer to its max height + await page.dragAndDrop( + findResizeHandlerByDrawerId(wrapper, drawerId).toSelector(), + 0, + toolbarTop - drawerTopInitial + ); + // make sure it does not overlap with toolbar after resizing + const { top: drawerTopAfterResizing } = await page.getBoundingBox( + findDrawerById(wrapper, drawerId)!.toSelector() + ); + expect(drawerTopAfterResizing).toBeGreaterThan(toolbarBottom); + + await page.click(wrapper.findSplitPanel().findOpenButton().toSelector()); + + // make sure it does not overlap with toolbar after opening a bottom split panel + const { top: drawerTopAfterOpeningSplitPanel } = await page.getBoundingBox( + findDrawerById(wrapper, drawerId)!.toSelector() + ); + const { top: splitPanelTop } = await page.getBoundingBox(wrapper.findSplitPanel().toSelector()); + expect(drawerTopAfterResizing).toBeGreaterThan(toolbarBottom); + + expect(drawerTopAfterOpeningSplitPanel).toBeGreaterThan(toolbarBottom); + expect(splitPanelTop).toBeGreaterThan(toolbarBottom); + }) + ); }); diff --git a/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap b/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap index 776ae9c761..26ace12fc2 100644 --- a/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap +++ b/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap @@ -9,6 +9,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -40,6 +42,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -74,6 +92,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -87,6 +106,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -109,6 +129,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -119,6 +140,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -202,6 +224,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -233,6 +257,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -267,6 +307,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -280,6 +321,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -302,6 +344,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -312,6 +355,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -355,6 +399,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -386,6 +432,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -420,6 +482,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -433,6 +496,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -455,6 +519,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -465,6 +530,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -539,6 +605,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -570,6 +638,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -604,6 +688,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -617,6 +702,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -639,6 +725,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -649,6 +736,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -692,6 +780,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -723,6 +813,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -757,6 +863,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -770,6 +877,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -792,6 +900,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -802,6 +911,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -850,6 +960,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -881,6 +993,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs":
breadcrumbs
, @@ -919,6 +1047,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -932,6 +1061,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation":
navigation @@ -956,6 +1086,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -966,6 +1097,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1052,6 +1184,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1083,6 +1217,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs":
breadcrumbs
, @@ -1121,6 +1271,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1134,6 +1285,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation":
navigation @@ -1158,6 +1310,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1168,6 +1321,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1211,6 +1365,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1242,6 +1398,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs":
breadcrumbs
, @@ -1280,6 +1452,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1293,6 +1466,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation":
navigation @@ -1317,6 +1491,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1327,6 +1502,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1373,6 +1549,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1404,6 +1582,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs":
breadcrumbs
, @@ -1442,6 +1636,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1455,6 +1650,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation":
navigation @@ -1479,6 +1675,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1489,6 +1686,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1565,6 +1763,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1596,6 +1796,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs":
breadcrumbs
, @@ -1634,6 +1850,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1647,6 +1864,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation":
navigation @@ -1671,6 +1889,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1681,6 +1900,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1724,6 +1944,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1755,6 +1977,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs":
breadcrumbs
, @@ -1793,6 +2031,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1806,6 +2045,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation":
navigation @@ -1830,6 +2070,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1840,6 +2081,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1902,6 +2144,8 @@ Map { }, }, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1933,6 +2177,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -1969,6 +2229,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1982,6 +2243,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -2004,6 +2266,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -2014,6 +2277,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -2113,6 +2377,8 @@ Map { }, }, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -2144,6 +2410,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -2180,6 +2462,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -2193,6 +2476,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -2215,6 +2499,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -2225,6 +2510,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -2282,6 +2568,8 @@ Map { }, }, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -2313,6 +2601,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -2349,6 +2653,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -2362,6 +2667,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -2384,6 +2690,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -2394,6 +2701,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -2482,6 +2790,8 @@ Map { }, }, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -2513,6 +2823,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -2549,6 +2875,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -2562,6 +2889,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -2584,6 +2912,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -2594,6 +2923,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -2651,6 +2981,8 @@ Map { }, }, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -2682,6 +3014,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -2718,6 +3066,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -2731,6 +3080,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -2753,6 +3103,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -2763,6 +3114,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], diff --git a/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel-old.test.tsx.snap b/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel-old.test.tsx.snap index 7d29d07a3b..d341c506ac 100644 --- a/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel-old.test.tsx.snap +++ b/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel-old.test.tsx.snap @@ -9,6 +9,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -40,6 +42,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -74,6 +92,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -87,6 +106,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -109,6 +129,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -119,6 +140,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -202,6 +224,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -233,6 +257,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -267,6 +307,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -280,6 +321,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -302,6 +344,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -312,6 +355,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -355,6 +399,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -386,6 +432,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -420,6 +482,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -433,6 +496,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -455,6 +519,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -465,6 +530,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -546,6 +612,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -577,6 +645,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -611,6 +695,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -624,6 +709,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -646,6 +732,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -656,6 +743,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -699,6 +787,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -730,6 +820,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -764,6 +870,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -777,6 +884,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -799,6 +907,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -809,6 +918,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -857,6 +967,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -888,6 +1000,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -922,6 +1050,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -935,6 +1064,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -957,6 +1087,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -967,6 +1098,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1050,6 +1182,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1081,6 +1215,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -1115,6 +1265,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1128,6 +1279,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -1150,6 +1302,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1160,6 +1313,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1203,6 +1357,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1234,6 +1390,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -1268,6 +1440,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1281,6 +1454,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -1303,6 +1477,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1313,6 +1488,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1393,6 +1569,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1424,6 +1602,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -1458,6 +1652,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1471,6 +1666,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -1493,6 +1689,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1503,6 +1700,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], @@ -1546,6 +1744,8 @@ Map { "activeAiDrawerSize": 0, "activeDrawer": undefined, "activeDrawerSize": 290, + "activeGlobalBottomDrawerId": null, + "activeGlobalBottomDrawerSize": 290, "activeGlobalDrawers": [], "activeGlobalDrawersIds": [], "activeGlobalDrawersSizes": {}, @@ -1577,6 +1777,22 @@ Map { "toolsClose": undefined, "toolsToggle": undefined, }, + "bottomDrawerReportedSize": 0, + "bottomDrawersFocusControl": { + "loseFocus": [Function], + "refs": { + "close": { + "current": null, + }, + "slider": { + "current": null, + }, + "toggle": { + "current": null, + }, + }, + "setFocus": [Function], + }, "breadcrumbs": undefined, "discoveredBreadcrumbs": null, "drawers": [ @@ -1611,6 +1827,7 @@ Map { }, "drawersOpenQueue": [], "expandedDrawerId": null, + "getMaxGlobalBottomDrawerHeight": [Function], "globalDrawers": [], "globalDrawersFocusControl": { "loseFocus": [Function], @@ -1624,6 +1841,7 @@ Map { "maxGlobalDrawersSizes": {}, "minAiDrawerSize": 400, "minDrawerSize": 290, + "minGlobalBottomDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , "navigationFocusControl": { @@ -1646,6 +1864,7 @@ Map { "onActiveAiDrawerResize": [Function], "onActiveDrawerChange": [Function], "onActiveDrawerResize": [Function], + "onActiveGlobalBottomDrawerChange": [Function], "onActiveGlobalDrawersChange": [Function], "onNavigationToggle": [Function], "onSplitPanelToggle": [Function], @@ -1656,6 +1875,7 @@ Map { "insetInlineEnd": 0, "insetInlineStart": 0, }, + "reportBottomDrawerSize": [Function], "setExpandedDrawerId": [Function], "setNotificationsHeight": [Function], "setToolbarHeight": [Function], diff --git a/src/app-layout/__tests__/runtime-drawers-layout.test.tsx b/src/app-layout/__tests__/runtime-drawers-layout.test.tsx index 34ea2b4523..409e4d61cf 100644 --- a/src/app-layout/__tests__/runtime-drawers-layout.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers-layout.test.tsx @@ -133,6 +133,69 @@ describe('toolbar mode only features', () => { expect(globalDrawersWrapper.findActiveDrawers()[1].getElement()).toHaveTextContent('global drawer content 2'); }); + test('(with a bottom drawer open) first opened drawer (global drawer) should be closed when active drawers take up all available space on the page and a third drawer is opened', async () => { + jest.mocked(computeHorizontalLayout).mockReturnValue({ + splitPanelPosition: 'bottom', + splitPanelForcedPosition: false, + sideSplitPanelSize: 0, + maxSplitPanelSize: 792, + maxDrawerSize: 792, + maxGlobalDrawersSizes: {}, + totalActiveGlobalDrawersSize: 0, + resizableSpaceAvailable: 792, + maxAiDrawerSize: 0, + }); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'local-drawer', + mountContent: container => (container.textContent = 'local-drawer content'), + }); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global-drawer-1', + type: 'global', + defaultActive: true, + mountContent: container => (container.textContent = 'global drawer content 1'), + }); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global-drawer-2', + type: 'global', + defaultActive: true, + mountContent: container => (container.textContent = 'global drawer content 2'), + }); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global-drawer-3', + mountContent: container => (container.textContent = 'global drawer content 3'), + }); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global-bottom-drawer', + type: 'global', + position: 'bottom', + defaultActive: true, + mountContent: container => (container.textContent = 'global bottom drawer'), + }); + const { wrapper, globalDrawersWrapper } = await renderComponent(); + + await delay(); + + expect(globalDrawersWrapper.findActiveDrawers()!.length).toBe(3); + expect(globalDrawersWrapper.findActiveDrawers()[0].getElement()).toHaveTextContent('global bottom drawer'); + expect(globalDrawersWrapper.findActiveDrawers()[1].getElement()).toHaveTextContent('global drawer content 1'); + expect(globalDrawersWrapper.findActiveDrawers()[2].getElement()).toHaveTextContent('global drawer content 2'); + + wrapper.findDrawerTriggerById('local-drawer')!.click(); + + await waitFor(() => { + expect(globalDrawersWrapper.findActiveDrawers()!.length).toBe(3); + }); + expect(globalDrawersWrapper.findActiveDrawers()[0].getElement()).toHaveTextContent('global bottom drawer'); + expect(globalDrawersWrapper.findActiveDrawers()[1].getElement()).toHaveTextContent('local-drawer'); + expect(globalDrawersWrapper.findActiveDrawers()[2].getElement()).toHaveTextContent('global drawer content 2'); + }); + test('first opened drawer (local drawer) should be closed when active drawers take up all available space on the page and a third drawer is opened', async () => { jest.mocked(computeHorizontalLayout).mockReturnValue({ splitPanelPosition: 'bottom', diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index 0e464510a9..657073f7ea 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -933,284 +933,289 @@ describe('toolbar mode only features', () => { }); describe.each(['global', 'global-ai'] as const)('drawer type = %s', type => { - const findDrawerTriggerById = (id: string, renderProps: Awaited>) => { - if (type === 'global') { - return renderProps.wrapper.findDrawerTriggerById(id); - } else { - return renderProps.globalDrawersWrapper.findAiDrawerTrigger(); + describe.each(['side', 'bottom'] as const)('drawer position = %s', position => { + // this condition does not exist + if (type === 'global-ai' && position === 'bottom') { + return; } - }; - const registerDrawer = (payload: DrawerConfig | WidgetDrawerPayload) => { - if (type === 'global') { - awsuiPlugins.appLayout.registerDrawer({ ...payload, type } as DrawerConfig); - } else { - 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({ - ...drawerDefaults, - id: 'test-resizable', - resizable: true, - ariaLabels: { - triggerButton: 'drawer trigger', - content: 'drawer content', - resizeHandle: 'drawer resize', - closeButton: 'drawer close', - }, - }); - const renderProps = await renderComponent(); - const { globalDrawersWrapper } = renderProps; + const findDrawerTriggerById = (id: string, renderProps: Awaited>) => { + if (type === 'global') { + return renderProps.wrapper.findDrawerTriggerById(id); + } else { + return renderProps.globalDrawersWrapper.findAiDrawerTrigger(); + } + }; + const registerDrawer = (payload: DrawerConfig | WidgetDrawerPayload) => { + if (type === 'global') { + awsuiPlugins.appLayout.registerDrawer({ ...payload, type, position } as DrawerConfig); + } else { + awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); + } + }; + const findDrawerHeaderActionById = (id: string, renderProps: Awaited>) => { + return createWrapper(renderProps.container).findButtonGroup()!.findButtonById(id); + }; - findDrawerTriggerById('test-resizable', renderProps)!.click(); + test('renders resize handle for a global drawer when config is enabled', async () => { + registerDrawer({ + ...drawerDefaults, + id: 'test-resizable', + resizable: true, + ariaLabels: { + triggerButton: 'drawer trigger', + content: 'drawer content', + resizeHandle: 'drawer resize', + closeButton: 'drawer close', + }, + }); + const renderProps = await renderComponent(); + const { globalDrawersWrapper } = renderProps; - await waitFor(() => { - expect(globalDrawersWrapper.findResizeHandleByActiveDrawerId('test-resizable')!.getElement()).toHaveFocus(); - expect(globalDrawersWrapper.findResizeHandleByActiveDrawerId('test-resizable')!.getElement()).toHaveAttribute( - 'aria-label', - 'drawer resize' - ); - }); - }); + findDrawerTriggerById('test-resizable', renderProps)!.click(); - test('close active global drawer by clicking on close button', async () => { - registerDrawer({ - ...drawerDefaults, - id: 'global-drawer', - ariaLabels: { - triggerButton: 'drawer trigger', - content: 'drawer content', - resizeHandle: 'drawer resize', - closeButton: 'drawer close', - }, + await waitFor(() => { + expect(globalDrawersWrapper.findResizeHandleByActiveDrawerId('test-resizable')!.getElement()).toHaveFocus(); + expect( + globalDrawersWrapper.findResizeHandleByActiveDrawerId('test-resizable')!.getElement() + ).toHaveAttribute('aria-label', 'drawer resize'); + }); }); - const renderProps = await renderComponent(); - const { globalDrawersWrapper } = renderProps; + test('close active global drawer by clicking on close button', async () => { + registerDrawer({ + ...drawerDefaults, + id: 'global-drawer', + ariaLabels: { + triggerButton: 'drawer trigger', + content: 'drawer content', + resizeHandle: 'drawer resize', + closeButton: 'drawer close', + }, + }); - findDrawerTriggerById('global-drawer', renderProps)!.click(); - expect(globalDrawersWrapper.findDrawerById('global-drawer')!.getElement()).toBeInTheDocument(); - renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); - expect(globalDrawersWrapper.findDrawerById('global-drawer')).toBeNull(); - }); + const renderProps = await renderComponent(); + const { globalDrawersWrapper } = renderProps; - test('opens a drawer when openDrawer is called', async () => { - awsuiPlugins.appLayout.registerDrawer({ - ...drawerDefaults, - id: 'local-drawer', - mountContent: container => (container.textContent = 'local-drawer content'), - }); - awsuiPlugins.appLayout.registerDrawer({ - ...drawerDefaults, - id: 'global-drawer-1', - type: 'global', - mountContent: container => (container.textContent = 'global drawer content 1'), - }); - registerDrawer({ - ...drawerDefaults, - id: 'global-drawer-2', - mountContent: container => (container.textContent = 'global drawer content 2'), + findDrawerTriggerById('global-drawer', renderProps)!.click(); + expect(globalDrawersWrapper.findDrawerById('global-drawer')!.getElement()).toBeInTheDocument(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); + expect(globalDrawersWrapper.findDrawerById('global-drawer')).toBeNull(); }); - const { globalDrawersWrapper } = await renderComponent(); + test('opens a drawer when openDrawer is called', async () => { + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'local-drawer', + mountContent: container => (container.textContent = 'local-drawer content'), + }); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global-drawer-1', + type: 'global', + mountContent: container => (container.textContent = 'global drawer content 1'), + }); + registerDrawer({ + ...drawerDefaults, + id: 'global-drawer-2', + mountContent: container => (container.textContent = 'global drawer content 2'), + }); - expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(0); + const { globalDrawersWrapper } = await renderComponent(); - awsuiPlugins.appLayout.openDrawer('local-drawer'); + expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(0); - await delay(); + awsuiPlugins.appLayout.openDrawer('local-drawer'); - expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(1); + await delay(); - awsuiPlugins.appLayout.openDrawer('global-drawer-1'); + expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(1); - await delay(); + awsuiPlugins.appLayout.openDrawer('global-drawer-1'); - expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(2); - }); + await delay(); - test('does not do anything when openDrawer is called with active drawer id', async () => { - registerDrawer({ - ...drawerDefaults, - id: 'local-drawer', - mountContent: container => (container.textContent = 'local-drawer content'), + expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(2); }); - const { globalDrawersWrapper } = await renderComponent(); + test('does not do anything when openDrawer is called with active drawer id', async () => { + registerDrawer({ + ...drawerDefaults, + id: 'local-drawer', + mountContent: container => (container.textContent = 'local-drawer content'), + }); - expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(0); + const { globalDrawersWrapper } = await renderComponent(); - if (type === 'global') { - awsuiPlugins.appLayout.openDrawer('local-drawer'); - } else { - awsuiWidgetPlugins.updateDrawer({ type: 'openDrawer', payload: { id: 'local-drawer' } }); - } + expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(0); - await delay(); + if (type === 'global') { + awsuiPlugins.appLayout.openDrawer('local-drawer'); + } else { + awsuiWidgetPlugins.updateDrawer({ type: 'openDrawer', payload: { id: 'local-drawer' } }); + } - expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(1); + await delay(); - if (type === 'global') { - awsuiPlugins.appLayout.openDrawer('local-drawer'); - } else { - awsuiWidgetPlugins.updateDrawer({ type: 'openDrawer', payload: { id: 'local-drawer' } }); - } + expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(1); - await delay(); + if (type === 'global') { + awsuiPlugins.appLayout.openDrawer('local-drawer'); + } else { + awsuiWidgetPlugins.updateDrawer({ type: 'openDrawer', payload: { id: 'local-drawer' } }); + } - expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(1); - }); + await delay(); - test('should restore focus when a global drawer is closed', async () => { - registerDrawer({ - ...drawerDefaults, - id: 'global-drawer-1', - mountContent: container => (container.textContent = 'global drawer content 1'), + expect(globalDrawersWrapper.findActiveDrawers()).toHaveLength(1); }); - const renderProps = await renderComponent(); - const { globalDrawersWrapper } = renderProps; + test('should restore focus when a global drawer is closed', async () => { + registerDrawer({ + ...drawerDefaults, + id: 'global-drawer-1', + mountContent: container => (container.textContent = 'global drawer content 1'), + }); - findDrawerTriggerById('global-drawer-1', renderProps)!.focus(); - findDrawerTriggerById('global-drawer-1', renderProps)!.click(); - expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); - renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); - expect(globalDrawersWrapper.findDrawerById('global-drawer-1')).toBeNull(); - await waitFor(() => { - expect(findDrawerTriggerById('global-drawer-1', renderProps)!.getElement()).toHaveFocus(); - }); - }); + const renderProps = await renderComponent(); + const { globalDrawersWrapper } = renderProps; - test('when preserveInactiveContent is set to true, initially closed drawer does not exist in dom (but mounted and persists when opened and closed)', async () => { - registerDrawer({ - ...drawerDefaults, - id: 'global-drawer-1', - mountContent: container => (container.textContent = 'global drawer content 1'), - preserveInactiveContent: true, + findDrawerTriggerById('global-drawer-1', renderProps)!.focus(); + findDrawerTriggerById('global-drawer-1', renderProps)!.click(); + expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + expect(globalDrawersWrapper.findDrawerById('global-drawer-1')).toBeNull(); + await waitFor(() => { + expect(findDrawerTriggerById('global-drawer-1', renderProps)!.getElement()).toHaveFocus(); + }); }); - const renderProps = await renderComponent(); - const { globalDrawersWrapper } = renderProps; + test('when preserveInactiveContent is set to true, initially closed drawer does not exist in dom (but mounted and persists when opened and closed)', async () => { + registerDrawer({ + ...drawerDefaults, + id: 'global-drawer-1', + mountContent: container => (container.textContent = 'global drawer content 1'), + preserveInactiveContent: true, + }); - expect(globalDrawersWrapper.findDrawerById('global-drawer-1')).toBeNull(); + const renderProps = await renderComponent(); + const { globalDrawersWrapper } = renderProps; - findDrawerTriggerById('global-drawer-1', renderProps)!.click(); + expect(globalDrawersWrapper.findDrawerById('global-drawer-1')).toBeNull(); - await delay(); + findDrawerTriggerById('global-drawer-1', renderProps)!.click(); - expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); - renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + await delay(); - expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); - expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(false); - }); + expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); - test('should call visibilityChange callback when global drawer with preserveInactiveContent is opened and closed', async () => { - const onVisibilityChangeMock = jest.fn(); - registerDrawer({ - ...drawerDefaults, - id: 'global-drawer-1', - mountContent: (container, mountContext) => { - if (mountContext?.onVisibilityChange) { - mountContext.onVisibilityChange(onVisibilityChangeMock); - } - container.textContent = 'global drawer content 1'; - }, - preserveInactiveContent: true, + expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); + expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(false); }); - const renderProps = await renderComponent(); - const { globalDrawersWrapper } = renderProps; - - findDrawerTriggerById('global-drawer-1', renderProps)!.click(); + test('should call visibilityChange callback when global drawer with preserveInactiveContent is opened and closed', async () => { + const onVisibilityChangeMock = jest.fn(); + registerDrawer({ + ...drawerDefaults, + id: 'global-drawer-1', + mountContent: (container, mountContext) => { + if (mountContext?.onVisibilityChange) { + mountContext.onVisibilityChange(onVisibilityChangeMock); + } + container.textContent = 'global drawer content 1'; + }, + preserveInactiveContent: true, + }); - await delay(); + const renderProps = await renderComponent(); + const { globalDrawersWrapper } = renderProps; - expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); - expect(onVisibilityChangeMock).toHaveBeenCalledWith(true); + findDrawerTriggerById('global-drawer-1', renderProps)!.click(); - renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); - expect(onVisibilityChangeMock).toHaveBeenCalledWith(false); - }); + await delay(); - test(`closes a drawer when closeDrawer is called (${type} drawer)`, async () => { - registerDrawer({ ...drawerDefaults, resizable: true }); + expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); + expect(onVisibilityChangeMock).toHaveBeenCalledWith(true); - const { wrapper } = await renderComponent(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + expect(onVisibilityChangeMock).toHaveBeenCalledWith(false); + }); - if (type === 'global') { - awsuiPlugins.appLayout.openDrawer('test'); - } else { - awsuiWidgetPlugins.updateDrawer({ type: 'openDrawer', payload: { id: 'test' } }); - } + test(`closes a drawer when closeDrawer is called (${type} drawer)`, async () => { + registerDrawer({ ...drawerDefaults, resizable: true }); - await delay(); + const { wrapper } = await renderComponent(); - expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('runtime drawer content'); + if (type === 'global') { + awsuiPlugins.appLayout.openDrawer('test'); + } else { + awsuiWidgetPlugins.updateDrawer({ type: 'openDrawer', payload: { id: 'test' } }); + } - if (type === 'global') { - awsuiPlugins.appLayout.closeDrawer('test'); - } else { - awsuiWidgetPlugins.updateDrawer({ type: 'closeDrawer', payload: { id: 'test' } }); - } + await delay(); - await delay(); + expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('runtime drawer content'); - expect(wrapper.findActiveDrawer()).toBeFalsy(); - }); + if (type === 'global') { + awsuiPlugins.appLayout.closeDrawer('test'); + } else { + awsuiWidgetPlugins.updateDrawer({ type: 'closeDrawer', payload: { id: 'test' } }); + } - test('should render trigger buttons for global drawers even if local drawers are not present', async () => { - const renderProps = await renderComponent(); + await delay(); - registerDrawer({ - ...drawerDefaults, - id: 'global1', + expect(wrapper.findActiveDrawer()).toBeFalsy(); }); - await delay(); + test('should render trigger buttons for global drawers even if local drawers are not present', async () => { + const renderProps = await renderComponent(); - expect(findDrawerTriggerById('global1', renderProps)!.getElement()).toBeInTheDocument(); - }); + registerDrawer({ + ...drawerDefaults, + id: 'global1', + }); - test(`calls onToggle handler by clicking on drawers trigger button (${type} runtime drawers)`, async () => { - const onToggle = jest.fn(); - registerDrawer({ - ...drawerDefaults, - id: 'global-drawer', - onToggle: event => onToggle(event.detail), + await delay(); + + expect(findDrawerTriggerById('global1', renderProps)!.getElement()).toBeInTheDocument(); }); - const renderProps = await renderComponent(); - findDrawerTriggerById('global-drawer', renderProps)!.click(); - expect(onToggle).toHaveBeenCalledWith({ isOpen: true, initiatedByUserAction: true }); - renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); - expect(onToggle).toHaveBeenCalledWith({ isOpen: false, initiatedByUserAction: true }); - }); + test(`calls onToggle handler by clicking on drawers trigger button (${type} runtime drawers)`, async () => { + const onToggle = jest.fn(); + registerDrawer({ + ...drawerDefaults, + id: 'global-drawer', + onToggle: event => onToggle(event.detail), + }); + const renderProps = await renderComponent(); - 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), + findDrawerTriggerById('global-drawer', renderProps)!.click(); + expect(onToggle).toHaveBeenCalledWith({ isOpen: true, initiatedByUserAction: true }); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); + expect(onToggle).toHaveBeenCalledWith({ isOpen: false, initiatedByUserAction: true }); }); - const renderProps = await renderComponent(); - findDrawerTriggerById('global-drawer', renderProps)!.click(); - findDrawerHeaderActionById('add', renderProps)!.click(); - expect(onHeaderActionClick).toHaveBeenCalledWith({ id: 'add' }); + 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' }); + }); }); }); @@ -1434,153 +1439,184 @@ describe('toolbar mode only features', () => { expect(globalDrawersWrapper.findDrawerById('global2')!.isActive()).toBe(true); expect(globalDrawersWrapper.findDrawerById('global3')).toBeFalsy(); }); + + test('should open global bottom drawer by default when defaultActive is set', async () => { + const { globalDrawersWrapper } = await renderComponent(); + + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global1', + type: 'global', + position: 'bottom', + defaultActive: true, + }); + + await delay(); + + expect(globalDrawersWrapper.findDrawerById('global1')!.isActive()).toBe(true); + }); }); describe('expanded mode for global drawers', () => { describe.each(['global', 'global-ai'] as const)('drawer type = %s', type => { - const findDrawerTriggerById = (id: string, renderProps: Awaited>) => { - if (type === 'global') { - return renderProps.wrapper.findDrawerTriggerById(id); - } else { - return renderProps.globalDrawersWrapper.findAiDrawerTrigger(); - } - }; - const registerDrawer = (payload: DrawerConfig | WidgetDrawerPayload) => { - if (type === 'global') { - awsuiPlugins.appLayout.registerDrawer({ ...payload, type } as DrawerConfig); - } else { - awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); + describe.each(['side', 'bottom'] as const)('drawer position = %s', position => { + // this condition does not exist + if (type === 'global-ai' && position === 'bottom') { + return; } - }; - test('should set a drawer to expanded mode by clicking on "expanded mode" button', async () => { - const drawerId = 'global-drawer'; - registerDrawer({ - ...drawerDefaults, - ariaLabels: { - expandedModeButton: 'Expanded mode button', - }, - id: drawerId, - isExpandable: true, - }); - const renderProps = await renderComponent(); - const { globalDrawersWrapper } = renderProps; - - findDrawerTriggerById(drawerId, renderProps)!.click(); - expect( - renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() - ).toBeInTheDocument(); - expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); - renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); - expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); - - expect( - getGeneratedAnalyticsMetadata( - renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() - ) - ).toEqual( - expect.objectContaining({ - action: 'expand', - detail: expect.objectContaining({ - label: 'Expanded mode button', - }), - }) - ); - - renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); - expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); - expect( - getGeneratedAnalyticsMetadata( + const findDrawerTriggerById = (id: string, renderProps: Awaited>) => { + if (type === 'global') { + return renderProps.wrapper.findDrawerTriggerById(id); + } else { + return renderProps.globalDrawersWrapper.findAiDrawerTrigger(); + } + }; + const registerDrawer = (payload: DrawerConfig | WidgetDrawerPayload) => { + if (type === 'global') { + awsuiPlugins.appLayout.registerDrawer({ ...payload, type, position } as DrawerConfig); + } else { + awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); + } + }; + + test('should set a drawer to expanded mode by clicking on "expanded mode" button', async () => { + const drawerId = 'global-drawer'; + registerDrawer({ + ...drawerDefaults, + ariaLabels: { + expandedModeButton: 'Expanded mode button', + }, + id: drawerId, + isExpandable: true, + }); + const renderProps = await renderComponent(); + const { globalDrawersWrapper } = renderProps; + + findDrawerTriggerById(drawerId, renderProps)!.click(); + expect( renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() - ) - ).toEqual( - expect.objectContaining({ - action: 'collapse', - detail: expect.objectContaining({ - label: 'Expanded mode button', - }), - }) - ); - }); + ).toBeInTheDocument(); + expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); + + expect( + getGeneratedAnalyticsMetadata( + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + ) + ).toEqual( + expect.objectContaining({ + action: 'expand', + detail: expect.objectContaining({ + label: 'Expanded mode button', + }), + }) + ); - test('only one drawer could be in expanded mode. all other panels should be closed', async () => { - const drawerId1 = 'global-drawer1'; - const drawerId2 = 'global-drawer2'; - const drawerId3Local = 'local-drawer'; - awsuiPlugins.appLayout.registerDrawer({ - ...drawerDefaults, - id: drawerId1, - type: 'global', - isExpandable: true, - }); - registerDrawer({ - ...drawerDefaults, - id: drawerId2, - isExpandable: true, - }); - awsuiPlugins.appLayout.registerDrawer({ - ...drawerDefaults, - id: drawerId3Local, + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); + expect( + getGeneratedAnalyticsMetadata( + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + ) + ).toEqual( + expect.objectContaining({ + action: 'collapse', + detail: expect.objectContaining({ + label: 'Expanded mode button', + }), + }) + ); }); - const renderProps = await renderComponent(nav
} />); - const { wrapper, globalDrawersWrapper } = renderProps; - - await delay(); - wrapper.findDrawerTriggerById(drawerId1)!.click(); - findDrawerTriggerById(drawerId2, renderProps)!.click(); - wrapper.findDrawerTriggerById(drawerId3Local)!.click(); + test('only one drawer could be in expanded mode. all other panels should be closed', async () => { + const drawerId1 = 'global-drawer1'; + const drawerId2 = 'global-drawer2'; + const drawerId3Local = 'local-drawer'; + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: drawerId1, + type: 'global', + isExpandable: true, + }); + registerDrawer({ + ...drawerDefaults, + id: drawerId2, + isExpandable: true, + }); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: drawerId3Local, + }); + const renderProps = await renderComponent(nav
} />); + const { wrapper, globalDrawersWrapper } = renderProps; + + await delay(); + + wrapper.findDrawerTriggerById(drawerId1)!.click(); + findDrawerTriggerById(drawerId2, renderProps)!.click(); + wrapper.findDrawerTriggerById(drawerId3Local)!.click(); - expect(wrapper.findDrawerTriggerById(drawerId1)!.getElement()).toHaveClass(toolbarTriggerStyles.selected); - // because the trigger button for the AI drawer gets hidden when it's expanded - if (type === 'global') { - expect(wrapper.findDrawerTriggerById(drawerId2)!.getElement()).toHaveClass(toolbarTriggerStyles.selected); - } - expect(wrapper.findDrawerTriggerById(drawerId3Local)!.getElement()).toHaveClass( - toolbarTriggerStyles.selected - ); - expect(wrapper.findNavigationToggle()!.getElement()).toHaveClass(toolbarTriggerStyles.selected); - expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(false); - expect(globalDrawersWrapper.findDrawerById(drawerId1)!.isDrawerInExpandedMode()).toBe(false); + expect(wrapper.findDrawerTriggerById(drawerId1)!.getElement()).toHaveClass(toolbarTriggerStyles.selected); + // because the trigger button for the AI drawer gets hidden when it's expanded + if (type === 'global') { + expect(wrapper.findDrawerTriggerById(drawerId2)!.getElement()).toHaveClass(toolbarTriggerStyles.selected); + } + expect(wrapper.findDrawerTriggerById(drawerId3Local)!.getElement()).toHaveClass( + toolbarTriggerStyles.selected + ); + expect(wrapper.findNavigationToggle()!.getElement()).toHaveClass(toolbarTriggerStyles.selected); + expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(false); + expect(globalDrawersWrapper.findDrawerById(drawerId1)!.isDrawerInExpandedMode()).toBe(false); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId1)!.click(); + globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId1)!.click(); - expect(globalDrawersWrapper.findDrawerById(drawerId1)!.isDrawerInExpandedMode()).toBe(true); - expect(wrapper.findDrawerTriggerById(drawerId1)!.getElement()).toHaveClass(toolbarTriggerStyles.selected); - // because the trigger button for the AI drawer gets hidden when it's expanded - if (type === 'global') { - expect(wrapper.findDrawerTriggerById(drawerId2)!.getElement()).not.toHaveClass( + expect(globalDrawersWrapper.findDrawerById(drawerId1)!.isDrawerInExpandedMode()).toBe(true); + expect(wrapper.findDrawerTriggerById(drawerId1)!.getElement()).toHaveClass(toolbarTriggerStyles.selected); + // because the trigger button for the AI drawer gets hidden when it's expanded + if (type === 'global') { + expect(wrapper.findDrawerTriggerById(drawerId2)!.getElement()).not.toHaveClass( + toolbarTriggerStyles.selected + ); + } + expect(wrapper.findDrawerTriggerById(drawerId3Local)!.getElement()).not.toHaveClass( toolbarTriggerStyles.selected ); - } - expect(wrapper.findDrawerTriggerById(drawerId3Local)!.getElement()).not.toHaveClass( - toolbarTriggerStyles.selected - ); - expect(wrapper.findNavigationToggle()!.getElement()).not.toHaveClass(toolbarTriggerStyles.selected); - }); - - test('should quit expanded mode when a drawer in expanded mode is closed', async () => { - const drawerId = 'global-drawer'; - registerDrawer({ - ...drawerDefaults, - id: drawerId, - isExpandable: true, + expect(wrapper.findNavigationToggle()!.getElement()).not.toHaveClass(toolbarTriggerStyles.selected); }); - const renderProps = await renderComponent(); - const { globalDrawersWrapper } = renderProps; - - await delay(); - findDrawerTriggerById(drawerId, renderProps)!.click(); - renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); - expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); - expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(true); - renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId(drawerId)!.click(); - expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(false); + test('should quit expanded mode when a drawer in expanded mode is closed', async () => { + const drawerId = 'global-drawer'; + registerDrawer({ + ...drawerDefaults, + id: drawerId, + isExpandable: true, + }); + const renderProps = await renderComponent(); + const { globalDrawersWrapper } = renderProps; + + await delay(); + + findDrawerTriggerById(drawerId, renderProps)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); + expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(true); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId(drawerId)!.click(); + expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(false); + }); }); }); - test.each(['expanded', 'split-panel', 'global-drawer', 'local-drawer', 'nav', 'global-ai-drawer'] as const)( + test.each([ + 'expanded', + 'split-panel', + 'global-drawer', + 'local-drawer', + 'nav', + 'global-ai-drawer', + 'bottom-drawer', + ] as const)( 'should return panels to their initial state after leaving expanded mode by clicking on %s button', async triggerName => { const drawerId1 = 'global-drawer1'; @@ -1595,6 +1631,7 @@ describe('toolbar mode only features', () => { }); awsuiPlugins.appLayout.registerDrawer({ ...drawerDefaults, + position: triggerName === 'bottom-drawer' ? 'bottom' : 'side', id: drawerId2, type: 'global', isExpandable: true, @@ -1648,6 +1685,8 @@ describe('toolbar mode only features', () => { globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId1)!.click(); } else if (triggerName === 'global-drawer') { wrapper.findDrawerTriggerById(drawerId2)!.click(); + } else if (triggerName === 'bottom-drawer') { + wrapper.findDrawerTriggerById(drawerId2)!.click(); } else if (triggerName === 'local-drawer') { wrapper.findDrawerTriggerById(drawerId3Local)!.click(); } else if (triggerName === 'nav') { @@ -1670,66 +1709,69 @@ describe('toolbar mode only features', () => { } ); - test('should return panels to their initial state after leaving expanded mode by clicking on a button in the overflow menu', async () => { - const drawerId1 = 'global-drawer1'; - const drawerId2 = 'global-drawer2'; - awsuiPlugins.appLayout.registerDrawer({ - ...drawerDefaults, - id: drawerId1, - type: 'global', - isExpandable: true, - }); - awsuiPlugins.appLayout.registerDrawer({ - ...drawerDefaults, - id: drawerId2, - type: 'global', - isExpandable: true, - }); - const { wrapper, globalDrawersWrapper } = await renderComponent(); + describe.each(['side', 'bottom'] as const)('drawer position = %s', position => { + test('should return panels to their initial state after leaving expanded mode by clicking on a button in the overflow menu', async () => { + const drawerId1 = 'global-drawer1'; + const drawerId2 = 'global-drawer2'; + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: drawerId1, + type: 'global', + isExpandable: true, + }); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: drawerId2, + position, + type: 'global', + isExpandable: true, + }); + const { wrapper, globalDrawersWrapper } = await renderComponent(); - await delay(); + await delay(); - const buttonDropdown = wrapper.findDrawersOverflowTrigger(); + const buttonDropdown = wrapper.findDrawersOverflowTrigger(); - buttonDropdown!.openDropdown(); - buttonDropdown!.findItemById(drawerId1)!.click(); - buttonDropdown!.openDropdown(); - buttonDropdown!.findItemById(drawerId2)!.click(); + buttonDropdown!.openDropdown(); + buttonDropdown!.findItemById(drawerId1)!.click(); + buttonDropdown!.openDropdown(); + buttonDropdown!.findItemById(drawerId2)!.click(); - buttonDropdown!.openDropdown(); - expect(buttonDropdown!.findItemById(drawerId1)!.getElement().firstElementChild).toHaveAttribute( - 'aria-checked', - 'true' - ); - expect(buttonDropdown!.findItemById(drawerId2)!.getElement().firstElementChild).toHaveAttribute( - 'aria-checked', - 'true' - ); + buttonDropdown!.openDropdown(); + expect(buttonDropdown!.findItemById(drawerId1)!.getElement().firstElementChild).toHaveAttribute( + 'aria-checked', + 'true' + ); + expect(buttonDropdown!.findItemById(drawerId2)!.getElement().firstElementChild).toHaveAttribute( + 'aria-checked', + 'true' + ); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId1)!.click(); + globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId1)!.click(); - expect(globalDrawersWrapper.findDrawerById(drawerId1)!.isDrawerInExpandedMode()).toBe(true); - buttonDropdown!.openDropdown(); - expect(buttonDropdown!.findItemById(drawerId1)!.getElement().firstElementChild).toHaveAttribute( - 'aria-checked', - 'true' - ); - expect(buttonDropdown!.findItemById(drawerId2)!.getElement().firstElementChild).toHaveAttribute( - 'aria-checked', - 'false' - ); - // leave expanded mode - buttonDropdown!.findItemById(drawerId2)!.click(); - buttonDropdown!.openDropdown(); - expect(buttonDropdown!.findItemById(drawerId1)!.getElement().firstElementChild).toHaveAttribute( - 'aria-checked', - 'true' - ); - expect(buttonDropdown!.findItemById(drawerId2)!.getElement().firstElementChild).toHaveAttribute( - 'aria-checked', - 'true' - ); - expect(globalDrawersWrapper.findDrawerById(drawerId1)!.isDrawerInExpandedMode()).toBe(false); + expect(globalDrawersWrapper.findDrawerById(drawerId1)!.isDrawerInExpandedMode()).toBe(true); + buttonDropdown!.openDropdown(); + expect(buttonDropdown!.findItemById(drawerId1)!.getElement().firstElementChild).toHaveAttribute( + 'aria-checked', + 'true' + ); + expect(buttonDropdown!.findItemById(drawerId2)!.getElement().firstElementChild).toHaveAttribute( + 'aria-checked', + 'false' + ); + // leave expanded mode + buttonDropdown!.findItemById(drawerId2)!.click(); + buttonDropdown!.openDropdown(); + expect(buttonDropdown!.findItemById(drawerId1)!.getElement().firstElementChild).toHaveAttribute( + 'aria-checked', + 'true' + ); + expect(buttonDropdown!.findItemById(drawerId2)!.getElement().firstElementChild).toHaveAttribute( + 'aria-checked', + 'true' + ); + expect(globalDrawersWrapper.findDrawerById(drawerId1)!.isDrawerInExpandedMode()).toBe(false); + }); }); describe('nested app layouts', () => { @@ -1817,6 +1859,29 @@ describe('toolbar mode only features', () => { expect(getGlobalDrawerWidth(globalDrawersWrapper, 'test1')).toEqual('801px'); expect(getGlobalDrawerWidth(globalDrawersWrapper, 'test2')).toEqual('600px'); }); + + test('calls onResize handler for bottom drawer', async () => { + const { wrapper, globalDrawersWrapper, debug } = await renderComponent(); + const onResize = jest.fn(); + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + type: 'global', + position: 'bottom', + resizable: true, + onResize: event => onResize(event.detail), + }); + + await delay(); + + wrapper.findDrawerTriggerById(drawerDefaults.id)!.click(); + const handle = globalDrawersWrapper.findResizeHandleByActiveDrawerId(drawerDefaults.id)!; + debug(handle.getElement()); + handle.fireEvent(new MouseEvent('pointerdown', { bubbles: true })); + handle.fireEvent(new MouseEvent('pointermove', { bubbles: true })); + handle.fireEvent(new MouseEvent('pointerup', { bubbles: true })); + + expect(onResize).toHaveBeenCalledWith({ size: expect.any(Number), id: drawerDefaults.id }); + }); }); describeEachAppLayout({ themes: ['refresh-toolbar'], sizes: ['mobile'] }, () => { diff --git a/src/app-layout/resize/styles.scss b/src/app-layout/resize/styles.scss index db2eab751e..ce52cfc847 100644 --- a/src/app-layout/resize/styles.scss +++ b/src/app-layout/resize/styles.scss @@ -10,7 +10,7 @@ @include styles.media-breakpoint-up(styles.$breakpoint-x-small) { @include styles.with-motion { transition: awsui.$motion-duration-refresh-only-medium; - transition-property: border-color, opacity, block-size, inset-block-start; + transition-property: border-color, opacity, block-size, inset-block-start, inset-block-end; } } } diff --git a/src/app-layout/runtime-drawer/index.tsx b/src/app-layout/runtime-drawer/index.tsx index 0336991328..1de41edbc7 100644 --- a/src/app-layout/runtime-drawer/index.tsx +++ b/src/app-layout/runtime-drawer/index.tsx @@ -19,6 +19,7 @@ import styles from './styles.css.js'; export interface RuntimeDrawer extends AppLayoutProps.Drawer { onToggle?: NonCancelableEventHandler; + position?: 'side' | 'bottom'; } export interface DrawersLayout { diff --git a/src/app-layout/utils/use-drawers.ts b/src/app-layout/utils/use-drawers.ts index 7f89bdd502..06222d68c9 100644 --- a/src/app-layout/utils/use-drawers.ts +++ b/src/app-layout/utils/use-drawers.ts @@ -12,6 +12,7 @@ import { sortByPriority } from '../../internal/plugins/helpers/utils'; import { AppLayoutProps } from '../interfaces'; import { convertRuntimeDrawers, DrawersLayout } from '../runtime-drawer'; import { togglesConfig } from '../toggles'; +import { InternalDrawer } from '../visual-refresh-toolbar/interfaces'; export const TOOLS_DRAWER_ID = 'awsui-internal-tools'; @@ -59,7 +60,9 @@ function useRuntimeDrawers( activeDrawerId: string | null, onActiveDrawerChange: (newDrawerId: string | null, { initiatedByUserAction }: OnChangeParams) => void, activeGlobalDrawersIds: Array, - onActiveGlobalDrawersChange: (newDrawerId: string, { initiatedByUserAction }: OnChangeParams) => void + onActiveGlobalDrawersChange: (newDrawerId: string, { initiatedByUserAction }: OnChangeParams) => void, + activeGlobalBottomDrawerId: string | null, + onActiveGlobalBottomDrawerChange: (newDrawerId: string | null, { initiatedByUserAction }: OnChangeParams) => void ) { const [runtimeDrawers, setRuntimeDrawers] = useState({ localBefore: [], @@ -68,11 +71,14 @@ function useRuntimeDrawers( }); const onLocalDrawerChangeStable = useStableCallback(onActiveDrawerChange); const onGlobalDrawersChangeStable = useStableCallback(onActiveGlobalDrawersChange); + const onBottomDrawerChangeStable = useStableCallback(onActiveGlobalBottomDrawerChange); const localDrawerWasOpenRef = useRef(false); localDrawerWasOpenRef.current = localDrawerWasOpenRef.current || !!activeDrawerId; const activeGlobalDrawersIdsRef = useRef>([]); activeGlobalDrawersIdsRef.current = activeGlobalDrawersIds; + const bottomDrawerWasOpenRef = useRef(false); + bottomDrawerWasOpenRef.current = bottomDrawerWasOpenRef.current || !!activeGlobalBottomDrawerId; useEffect(() => { if (disableRuntimeDrawers) { @@ -88,8 +94,18 @@ function useRuntimeDrawers( onLocalDrawerChangeStable(defaultActiveLocalDrawer.id, { initiatedByUserAction: false }); } } + if (!bottomDrawerWasOpenRef.current) { + const defaultActiveBottomDrawer = sortByPriority(globalDrawers).find( + drawer => drawer?.position === 'bottom' && drawer.defaultActive + ); + if (defaultActiveBottomDrawer) { + onBottomDrawerChangeStable(defaultActiveBottomDrawer.id, { initiatedByUserAction: false }); + } + } - const drawersNotActiveByDefault = globalDrawers.filter(drawer => !drawer.defaultActive); + const drawersNotActiveByDefault = globalDrawers.filter( + drawer => !drawer.defaultActive && drawer.position !== 'bottom' + ); const hasDrawersOpenByUserAction = drawersNotActiveByDefault.find(drawer => activeGlobalDrawersIdsRef.current.includes(drawer.id) ); @@ -108,19 +124,21 @@ function useRuntimeDrawers( unsubscribe(); setRuntimeDrawers({ localBefore: [], localAfter: [], global: [] }); }; - }, [disableRuntimeDrawers, onGlobalDrawersChangeStable, onLocalDrawerChangeStable]); + }, [disableRuntimeDrawers, onBottomDrawerChangeStable, onGlobalDrawersChangeStable, onLocalDrawerChangeStable]); return runtimeDrawers; } function useDrawerRuntimeOpenClose( disableRuntimeDrawers: boolean | undefined, - localDrawers: AppLayoutProps.Drawer[] | null, - globalDrawers: AppLayoutProps.Drawer[], + localDrawers: InternalDrawer[] | null, + globalDrawers: InternalDrawer[], activeDrawerId: string | null, onActiveDrawerChange: (newDrawerId: string | null, { initiatedByUserAction }: OnChangeParams) => void, activeGlobalDrawersIds: Array, - onActiveGlobalDrawersChange: (newDrawerId: string, { initiatedByUserAction }: OnChangeParams) => void + onActiveGlobalDrawersChange: (newDrawerId: string, { initiatedByUserAction }: OnChangeParams) => void, + activeGlobalBottomDrawerId: string | null, + onActiveGlobalBottomDrawerChange: (newDrawerId: string | null, { initiatedByUserAction }: OnChangeParams) => void ) { const onDrawerOpened: DrawersToggledListener = useStableCallback((drawerId, params = DEFAULT_ON_CHANGE_PARAMS) => { const localDrawer = localDrawers?.find(drawer => drawer.id === drawerId); @@ -128,9 +146,13 @@ function useDrawerRuntimeOpenClose( if (localDrawer && activeDrawerId !== drawerId) { onActiveDrawerChange(drawerId, params); } - if (globalDrawer && !activeGlobalDrawersIds.includes(drawerId)) { + const isBottom = globalDrawer?.position === 'bottom'; + if (globalDrawer && !isBottom && !activeGlobalDrawersIds.includes(drawerId)) { onActiveGlobalDrawersChange(drawerId, params); } + if (globalDrawer && isBottom && activeGlobalBottomDrawerId !== drawerId) { + onActiveGlobalBottomDrawerChange(drawerId, params); + } }); const onDrawerClosed: DrawersToggledListener = useStableCallback((drawerId, params = DEFAULT_ON_CHANGE_PARAMS) => { @@ -139,9 +161,13 @@ function useDrawerRuntimeOpenClose( if (localDrawer && activeDrawerId === drawerId) { onActiveDrawerChange(null, params); } - if (globalDrawer && activeGlobalDrawersIds.includes(drawerId)) { + const isBottom = globalDrawer?.position === 'bottom'; + if (globalDrawer && !isBottom && activeGlobalDrawersIds.includes(drawerId)) { onActiveGlobalDrawersChange(drawerId, params); } + if (globalDrawer && isBottom && activeGlobalBottomDrawerId === drawerId) { + onActiveGlobalBottomDrawerChange(null, params); + } }); useEffect(() => { @@ -194,6 +220,7 @@ export const MIN_DRAWER_SIZE = 290; type UseDrawersProps = Pick & { __disableRuntimeDrawers?: boolean; onGlobalDrawerFocus?: (drawerId: string, open: boolean) => void; + onGlobalBottomDrawerFocus?: () => void; onAddNewActiveDrawer?: (drawerId: string) => void; }; @@ -204,6 +231,7 @@ export function useDrawers( onDrawerChange, onGlobalDrawerFocus, onAddNewActiveDrawer, + onGlobalBottomDrawerFocus, __disableRuntimeDrawers: disableRuntimeDrawers, }: UseDrawersProps, ariaLabels: AppLayoutProps['ariaLabels'], @@ -215,6 +243,7 @@ export function useDrawers( changeHandler: 'onChange', }); const [activeGlobalDrawersIds, setActiveGlobalDrawersIds] = useState>([]); + const [activeGlobalBottomDrawerId, setActiveGlobalBottomDrawerId] = useState(null); const [drawerSizes, setDrawerSizes] = useState>({}); const [expandedDrawerId, setExpandedDrawerId] = useState(null); // FIFO queue that keeps track of open drawers, where the first element is the most recently opened drawer @@ -222,7 +251,9 @@ export function useDrawers( function onActiveDrawerResize({ id, size }: { id: string; size: number }) { setDrawerSizes(oldSizes => ({ ...oldSizes, [id]: size })); - fireNonCancelableEvent(activeDrawer?.onResize, { id, size }); + if (activeDrawer?.id === id) { + fireNonCancelableEvent(activeDrawer?.onResize, { id, size }); + } const activeGlobalDrawer = runtimeGlobalDrawers.find(drawer => drawer.id === id); fireNonCancelableEvent(activeGlobalDrawer?.onResize, { id, size }); } @@ -280,6 +311,24 @@ export function useDrawers( } } + function onActiveGlobalBottomDrawerChange( + drawerId: string | null, + { initiatedByUserAction }: Partial = DEFAULT_ON_CHANGE_PARAMS + ) { + const drawer = runtimeGlobalDrawers.find(drawer => drawer.id === (drawerId || activeGlobalBottomDrawerId)); + setActiveGlobalBottomDrawerId(drawerId); + fireNonCancelableEvent(drawer?.onToggle, { isOpen: !!drawerId, initiatedByUserAction }); + if (activeGlobalBottomDrawerId === expandedDrawerId) { + setExpandedDrawerId(null); + } + if (drawerId) { + drawersOpenQueue.current = [drawerId, ...drawersOpenQueue.current]; + } else { + drawersOpenQueue.current = drawersOpenQueue.current.filter(id => id !== activeGlobalBottomDrawerId); + } + onGlobalBottomDrawerFocus?.(); + } + const hasOwnDrawers = !!drawers; // support toolsOpen in runtime-drawers-only mode let activeDrawerIdResolved = @@ -293,7 +342,9 @@ export function useDrawers( activeDrawerIdResolved, onActiveDrawerChange, activeGlobalDrawersIds, - onActiveGlobalDrawersChange + onActiveGlobalDrawersChange, + activeGlobalBottomDrawerId, + onActiveGlobalBottomDrawerChange ); const { localBefore, localAfter, global: runtimeGlobalDrawers } = runtimeDrawers; const combinedLocalDrawers = drawers @@ -303,6 +354,7 @@ export function useDrawers( // ensure that id is only defined when the drawer exists activeDrawerIdResolved = activeDrawer?.id ?? null; const activeGlobalDrawers = runtimeGlobalDrawers.filter(drawer => activeGlobalDrawersIds.includes(drawer.id)); + const activeGlobalBottomDrawer = runtimeGlobalDrawers?.find(drawer => drawer.id === activeGlobalBottomDrawerId); useDrawerRuntimeOpenClose( disableRuntimeDrawers, @@ -311,7 +363,9 @@ export function useDrawers( activeDrawerId, onActiveDrawerChange, activeGlobalDrawersIds, - onActiveGlobalDrawersChange + onActiveGlobalDrawersChange, + activeGlobalBottomDrawerId, + onActiveGlobalBottomDrawerChange ); useDrawerRuntimeResize(disableRuntimeDrawers, onActiveDrawerResize); @@ -340,6 +394,10 @@ export function useDrawers( toolsProps?.toolsOpen ? toolsProps.toolsWidth : (activeDrawer?.defaultSize ?? MIN_DRAWER_SIZE), MIN_DRAWER_SIZE ); + const activeGlobalBottomDrawerSize = activeGlobalBottomDrawerId + ? (drawerSizes[activeGlobalBottomDrawerId] ?? activeGlobalBottomDrawer?.defaultSize ?? MIN_DRAWER_SIZE) + : MIN_DRAWER_SIZE; + const minGlobalBottomDrawerSize = Math.min(activeGlobalBottomDrawer?.defaultSize ?? MIN_DRAWER_SIZE, MIN_DRAWER_SIZE); return { ariaLabelsWithDrawers: ariaLabels, @@ -360,5 +418,9 @@ export function useDrawers( onActiveGlobalDrawersChange, expandedDrawerId, setExpandedDrawerId, + activeGlobalBottomDrawerId, + onActiveGlobalBottomDrawerChange, + activeGlobalBottomDrawerSize, + minGlobalBottomDrawerSize, }; } diff --git a/src/app-layout/visual-refresh-toolbar/compute-layout.ts b/src/app-layout/visual-refresh-toolbar/compute-layout.ts index cc62684b1a..88df545dc0 100644 --- a/src/app-layout/visual-refresh-toolbar/compute-layout.ts +++ b/src/app-layout/visual-refresh-toolbar/compute-layout.ts @@ -150,12 +150,13 @@ export function computeSplitPanelOffsets({ export function getDrawerStyles( verticalOffsets: VerticalLayoutOutput, isMobile: boolean, - placement: AppLayoutPropsWithDefaults['placement'] + placement: AppLayoutPropsWithDefaults['placement'], + activeGlobalBottomDrawerSize: number = 0 ): { drawerTopOffset: number; drawerHeight: string; } { const drawerTopOffset = isMobile ? verticalOffsets.toolbar : (verticalOffsets.drawers ?? placement.insetBlockStart); - const drawerHeight = `calc(100vh - ${drawerTopOffset}px - ${placement.insetBlockEnd}px)`; + const drawerHeight = `calc(100vh - ${drawerTopOffset}px - ${placement.insetBlockEnd}px - ${activeGlobalBottomDrawerSize}px)`; return { drawerTopOffset, drawerHeight }; } diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-bottom-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-bottom-drawer.tsx new file mode 100644 index 0000000000..b74bfb2ca1 --- /dev/null +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-bottom-drawer.tsx @@ -0,0 +1,310 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useEffect, useRef } from 'react'; +import { Transition } from 'react-transition-group'; +import clsx from 'clsx'; + +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'; + +import sharedStyles from '../../resize/styles.css.js'; +import testutilStyles from '../../test-classes/styles.css.js'; +import styles from './styles.css.js'; + +export function AppLayoutBottomDrawerWrapper({ appLayoutInternals }: { appLayoutInternals: AppLayoutInternals }) { + const { activeGlobalBottomDrawerId, globalDrawers } = appLayoutInternals; + const openBottomDrawersHistory = useRef>(new Set()); + const bottomDrawers = globalDrawers.filter(drawer => drawer.position === 'bottom'); + useEffect(() => { + if (activeGlobalBottomDrawerId) { + openBottomDrawersHistory.current.add(activeGlobalBottomDrawerId); + } + }, [activeGlobalBottomDrawerId]); + + return ( + <> + {bottomDrawers.map(drawer => { + return ( + + ); + })} + + ); +} + +interface AppLayoutGlobalDrawerImplementationProps { + appLayoutInternals: AppLayoutInternals; + show: boolean; + activeDrawer: InternalDrawer | undefined; +} + +const GAP_HEIGHT = 10; +const RESIZE_HANDLER_HEIGHT = 18; + +function AppLayoutGlobalBottomDrawerImplementation({ + appLayoutInternals, + show, + activeDrawer, +}: AppLayoutGlobalDrawerImplementationProps) { + const { + ariaLabels, + isMobile, + onActiveGlobalBottomDrawerChange, + onActiveDrawerResize, + minGlobalBottomDrawerSize, + activeGlobalBottomDrawerSize, + drawersOpenQueue, + expandedDrawerId, + setExpandedDrawerId, + activeAiDrawer, + bottomDrawersFocusControl, + getMaxGlobalBottomDrawerHeight, + reportBottomDrawerSize, + verticalOffsets, + placement, + } = appLayoutInternals; + const drawerRef = useRef(null); + const activeDrawerId = activeDrawer?.id ?? ''; + + const computedAriaLabels = { + closeButton: activeDrawer ? activeDrawer.ariaLabels?.closeButton : ariaLabels?.toolsClose, + content: activeDrawer ? activeDrawer.ariaLabels?.drawerName : ariaLabels?.tools, + }; + + const { drawerTopOffset: mobileDrawerTopOffset, drawerHeight: drawerFullScreenHeight } = getDrawerStyles( + verticalOffsets, + isMobile, + placement + ); + const activeDrawerSize = activeGlobalBottomDrawerSize ?? 0; + const minDrawerSize = minGlobalBottomDrawerSize ?? 0; + const maxDrawerSize = getMaxGlobalBottomDrawerHeight(); + const refs = bottomDrawersFocusControl.refs; + const resizeProps = useResize({ + currentWidth: activeDrawerSize, + minWidth: minDrawerSize, + maxWidth: maxDrawerSize, + panelRef: drawerRef, + handleRef: refs?.slider, + onResize: size => onActiveDrawerResize({ id: activeDrawerId!, size }), + position: 'bottom', + }); + const size = getLimitedValue(minDrawerSize, activeDrawerSize, maxDrawerSize); + const lastOpenedDrawerId = drawersOpenQueue.length ? drawersOpenQueue[0] : null; + const hasTriggerButton = !!activeDrawer?.trigger; + const isExpanded = activeDrawer?.isExpandable && expandedDrawerId === activeDrawerId; + const wasExpanded = usePrevious(isExpanded); + const animationDisabled = + (activeDrawer?.defaultActive && !drawersOpenQueue.includes(activeDrawer.id)) || (wasExpanded && !isExpanded); + + // Prevent main content scroll when bottom drawer opens with animations + useEffect(() => { + if (!animationDisabled && show && drawerRef.current) { + const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; + const abortController = new AbortController(); + + // Temporarily prevent scrolling during animation + const preventScroll = () => { + document.documentElement.scrollTop = scrollTop; + document.body.scrollTop = scrollTop; + }; + + // Handle transition end to remove scroll prevention + const handleTransitionEnd = (event: TransitionEvent) => { + // Only handle transitions on the drawer element itself + if (event.target === drawerRef.current) { + abortController.abort(); + } + }; + + // Add scroll prevention during animation + document.addEventListener('scroll', preventScroll, { + passive: false, + signal: abortController.signal, + }); + + drawerRef.current.addEventListener('transitionend', handleTransitionEnd, { + signal: abortController.signal, + }); + + return () => { + abortController.abort(); + }; + } + }, [show, animationDisabled]); + + let drawerActions: ReadonlyArray = [ + { + type: 'icon-button', + id: 'close', + iconName: isMobile ? 'close' : 'angle-down', + text: computedAriaLabels.closeButton ?? '', + analyticsAction: 'close', + }, + ]; + if (!isMobile && activeDrawer?.isExpandable) { + drawerActions = [ + { + type: 'icon-button', + id: 'expand', + iconName: isExpanded ? 'shrink' : 'expand', + text: activeDrawer?.ariaLabels?.expandedModeButton ?? '', + analyticsAction: isExpanded ? 'expand' : 'collapse', + }, + ...drawerActions, + ]; + } + if (activeDrawer?.headerActions) { + drawerActions = [ + { + type: 'group', + text: 'Actions', + items: activeDrawer.headerActions!, + }, + ...drawerActions, + ]; + } + + useEffect(() => { + reportBottomDrawerSize(size); + }, [reportBottomDrawerSize, size]); + + return ( + + {state => { + return ( + + ); + }} + + ); +} + +export default AppLayoutGlobalBottomDrawerImplementation; diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawers.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawers.tsx index 9aabec89c1..4afe0eef34 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawers.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawers.tsx @@ -24,7 +24,7 @@ export function AppLayoutGlobalDrawersImplementation({ {globalDrawers .filter( drawer => - activeGlobalDrawersIds.includes(drawer.id) || + (drawer.position !== 'bottom' && activeGlobalDrawersIds.includes(drawer.id)) || (drawer.preserveInactiveContent && openDrawersHistory.current.has(drawer.id)) ) .map(drawer => { diff --git a/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx index 1e9ef561ce..5ada3165d7 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx @@ -36,6 +36,8 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD drawersOpenQueue, onActiveDrawerChange, onActiveDrawerResize, + activeGlobalBottomDrawerId, + bottomDrawerReportedSize, } = appLayoutInternals; const drawerRef = useRef(null); const activeDrawerId = activeDrawer?.id; @@ -45,7 +47,12 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD content: activeDrawer ? activeDrawer.ariaLabels?.drawerName : ariaLabels?.tools, }; - const { drawerTopOffset, drawerHeight } = getDrawerStyles(verticalOffsets, isMobile, placement); + const { drawerTopOffset, drawerHeight } = getDrawerStyles( + verticalOffsets, + isMobile, + placement, + activeGlobalBottomDrawerId ? bottomDrawerReportedSize : 0 + ); const toolsOnlyMode = drawers.length === 1 && drawers[0].id === TOOLS_DRAWER_ID; const isToolsDrawer = activeDrawer?.id === TOOLS_DRAWER_ID || toolsOnlyMode; const toolsContent = drawers?.find(drawer => drawer.id === TOOLS_DRAWER_ID)?.content; diff --git a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss index e59fad8595..7fc62b8958 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss @@ -44,6 +44,7 @@ $ai-drawer-heider-height: 41px; @include styles.with-motion { transition: inline-size #{$global-drawer-expanded-mode-motion}, + block-size #{$global-drawer-expanded-mode-motion}, min-inline-size #{$global-drawer-expanded-mode-motion}; } } @@ -416,4 +417,43 @@ $ai-drawer-heider-height: 41px; } } } + + &.bottom-drawer { + display: block; + inline-size: 100%; + block-size: var(#{custom-props.$bottomDrawerSize}); + + @include mobile-only { + block-size: 100%; + &.last-opened { + z-index: constants.$drawer-z-index-mobile; + } + } + + &.drawer-hidden { + display: none; + } + + > .global-drawer-wrapper { + display: block; + + > .drawer-gap { + block-size: $global-drawer-gap-size; + inline-size: 100%; + border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; + border-block-end: awsui.$border-divider-section-width solid awsui.$color-border-layout; + box-sizing: content-box; + } + + > .drawer-slider { + block-size: auto; + justify-content: center; + } + + > .drawer-content-container { + grid-template-columns: 1fr; + grid-template-rows: auto; + } + } + } } diff --git a/src/app-layout/visual-refresh-toolbar/drawer/use-resize.ts b/src/app-layout/visual-refresh-toolbar/drawer/use-resize.ts index ce1dac7ea4..0375398ea5 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/use-resize.ts +++ b/src/app-layout/visual-refresh-toolbar/drawer/use-resize.ts @@ -14,7 +14,7 @@ interface ResizeProps { panelRef: React.RefObject; handleRef: React.RefObject; onResize: (newWidth: number) => void; - position?: 'side-start' | 'side'; + position?: 'side-start' | 'side' | 'bottom'; } export function useResize({ currentWidth, minWidth, maxWidth, panelRef, handleRef, onResize, position }: ResizeProps) { diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index 968a690e95..0a3a868092 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -26,6 +26,7 @@ export type InternalDrawer = AppLayoutProps.Drawer & { header?: React.ReactNode; headerActions?: ReadonlyArray; onHeaderActionClick?: NonCancelableEventHandler; + position?: 'side' | 'bottom'; }; // Widgetization notice: structures in this file are shared multiple app layout instances, possibly different minor versions. @@ -52,6 +53,7 @@ export interface AppLayoutInternals { maxGlobalDrawersSizes: Record; drawers: ReadonlyArray; drawersFocusControl: FocusControlState; + bottomDrawersFocusControl: FocusControlState; globalDrawersFocusControl: FocusControlMultipleStates; activeGlobalDrawersIds: ReadonlyArray; activeGlobalDrawers: ReadonlyArray; @@ -83,6 +85,13 @@ export interface AppLayoutInternals { maxAiDrawerSize?: number; aiDrawerFocusControl?: FocusControlState; onActiveAiDrawerResize: (size: number) => void; + activeGlobalBottomDrawerId: string | null; + onActiveGlobalBottomDrawerChange: (value: string | null, params: OnChangeParams) => void; + activeGlobalBottomDrawerSize: number; + minGlobalBottomDrawerSize: number; + bottomDrawerReportedSize: number; + getMaxGlobalBottomDrawerHeight: () => number; + reportBottomDrawerSize: (size: number) => void; } interface AppLayoutWidgetizedState extends AppLayoutInternals { diff --git a/src/app-layout/visual-refresh-toolbar/navigation/index.tsx b/src/app-layout/visual-refresh-toolbar/navigation/index.tsx index 7aaae06ab5..23a4f6b1b3 100644 --- a/src/app-layout/visual-refresh-toolbar/navigation/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/navigation/index.tsx @@ -27,9 +27,16 @@ export function AppLayoutNavigationImplementation({ appLayoutInternals }: AppLay navigationFocusControl, placement, verticalOffsets, + activeGlobalBottomDrawerId, + bottomDrawerReportedSize, } = appLayoutInternals; - const { drawerTopOffset, drawerHeight } = getDrawerStyles(verticalOffsets, isMobile, placement); + const { drawerTopOffset, drawerHeight } = getDrawerStyles( + verticalOffsets, + isMobile, + placement, + activeGlobalBottomDrawerId ? bottomDrawerReportedSize : 0 + ); // Close the Navigation drawer on mobile when a user clicks a link inside. const onNavigationClick = (event: React.MouseEvent) => { diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss index 768d115d49..c0713f92bb 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss @@ -53,7 +53,8 @@ grid-template-areas: 'ai-drawer toolbar toolbar toolbar toolbar toolbar toolbar toolbar' 'ai-drawer navigation . notifications . sideSplitPanel tools global-tools' - 'ai-drawer navigation . main . sideSplitPanel tools global-tools'; + 'ai-drawer navigation . main . sideSplitPanel tools global-tools' + 'ai-drawer bottom-tool bottom-tool bottom-tool bottom-tool bottom-tool bottom-tool global-tools'; grid-template-columns: min-content min-content @@ -62,7 +63,7 @@ minmax(#{awsui.$space-layout-content-horizontal}, 1fr) min-content min-content; - grid-template-rows: min-content min-content 1fr min-content; + grid-template-rows: min-content min-content 1fr min-content min-content; &.has-adaptive-widths-default { #{custom-props.$maxContentWidth}: map.get(constants.$adaptive-content-widths, styles.$breakpoint-xx-large); @@ -98,6 +99,11 @@ 0 0; } + + &.bottom-drawer-expanded-mode { + grid-template-rows: auto; + grid-template-columns: 0 0 0 0 0 auto 0 0; + } } } } @@ -105,7 +111,8 @@ .ai-drawer, .navigation, .tools, -.global-tools { +.global-tools, +.bottom-tool { grid-row: 1 / -1; grid-column: 1 / -1; background: awsui.$color-background-container-content; @@ -116,6 +123,16 @@ } } +.bottom-tool { + @include desktop-only { + grid-area: bottom-tool; + position: sticky; + inset-block-end: 0; + overflow: hidden; + z-index: 840; + } +} + .ai-drawer { @include desktop-only { grid-area: ai-drawer; diff --git a/src/app-layout/visual-refresh-toolbar/split-panel/index.tsx b/src/app-layout/visual-refresh-toolbar/split-panel/index.tsx index 610f5f94d2..05163bb98f 100644 --- a/src/app-layout/visual-refresh-toolbar/split-panel/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/split-panel/index.tsx @@ -19,8 +19,21 @@ export function AppLayoutSplitPanelDrawerSideImplementation({ appLayoutInternals, splitPanelInternals, }: AppLayoutSplitPanelDrawerSideImplementationProps) { - const { splitPanelControlId, placement, verticalOffsets, isMobile, splitPanelAnimationDisabled } = appLayoutInternals; - const { drawerTopOffset, drawerHeight } = getDrawerStyles(verticalOffsets, isMobile, placement); + const { + splitPanelControlId, + placement, + verticalOffsets, + isMobile, + splitPanelAnimationDisabled, + activeGlobalBottomDrawerId, + bottomDrawerReportedSize, + } = appLayoutInternals; + const { drawerTopOffset, drawerHeight } = getDrawerStyles( + verticalOffsets, + isMobile, + placement, + activeGlobalBottomDrawerId ? bottomDrawerReportedSize : 0 + ); return ( diff --git a/src/app-layout/visual-refresh-toolbar/state/interfaces.ts b/src/app-layout/visual-refresh-toolbar/state/interfaces.ts index f391d1a6ac..1cc000895d 100644 --- a/src/app-layout/visual-refresh-toolbar/state/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/state/interfaces.ts @@ -18,6 +18,7 @@ export interface SharedProps { drawers: ReadonlyArray | undefined; onActiveDrawerChange: ((drawerId: string | null, params: OnChangeParams) => void) | undefined; drawersFocusRef: React.Ref | undefined; + bottomDrawersFocusRef?: React.Ref | undefined; globalDrawersFocusControl?: FocusControlMultipleStates | undefined; globalDrawers?: ReadonlyArray | undefined; activeGlobalDrawersIds?: ReadonlyArray | undefined; @@ -30,6 +31,8 @@ export interface SharedProps { setExpandedDrawerId?: (value: string | null) => void; aiDrawer?: AppLayoutProps.Drawer | undefined; aiDrawerFocusRef: React.Ref | undefined; + activeGlobalBottomDrawerId?: string | null; + onActiveGlobalBottomDrawerChange?: (value: string | null, params: OnChangeParams) => void; } export type MergeProps = ( diff --git a/src/app-layout/visual-refresh-toolbar/state/props-merger.ts b/src/app-layout/visual-refresh-toolbar/state/props-merger.ts index e610066e54..e6e2f79d4d 100644 --- a/src/app-layout/visual-refresh-toolbar/state/props-merger.ts +++ b/src/app-layout/visual-refresh-toolbar/state/props-merger.ts @@ -33,9 +33,12 @@ export const mergeProps: MergeProps = (ownProps, additionalProps) => { } if (props.globalDrawers && !checkAlreadyExists(!!toolbar.globalDrawers, 'globalDrawers')) { toolbar.globalDrawersFocusControl = props.globalDrawersFocusControl; + toolbar.bottomDrawersFocusRef = props.bottomDrawersFocusRef; toolbar.globalDrawers = props.globalDrawers; toolbar.activeGlobalDrawersIds = props.activeGlobalDrawersIds; toolbar.onActiveGlobalDrawersChange = props.onActiveGlobalDrawersChange; + toolbar.activeGlobalBottomDrawerId = props.activeGlobalBottomDrawerId; + toolbar.onActiveGlobalBottomDrawerChange = props.onActiveGlobalBottomDrawerChange; } if ( props.aiDrawer && @@ -89,7 +92,10 @@ export const getPropsToMerge = (props: AppLayoutInternalProps, appLayoutState: A activeGlobalDrawersIds: state?.activeGlobalDrawersIds, onActiveGlobalDrawersChange: state?.onActiveGlobalDrawersChange, onActiveDrawerChange: state?.onActiveDrawerChange, + activeGlobalBottomDrawerId: state?.activeGlobalBottomDrawerId, + onActiveGlobalBottomDrawerChange: state?.onActiveGlobalBottomDrawerChange, drawersFocusRef: state?.drawersFocusControl.refs.toggle, + bottomDrawersFocusRef: state?.bottomDrawersFocusControl.refs.toggle, splitPanel: props.splitPanel, splitPanelToggleProps: state?.splitPanelToggleConfig && { ...state.splitPanelToggleConfig, 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 7ab4d17a32..20c2741180 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 @@ -90,10 +90,10 @@ export const useAppLayout = ( }; const onAddNewActiveDrawer = (drawerId: string) => { - // If a local drawer is already open, and we attempt to open a new one, + // If either a local drawer or a bottom drawer is already open, and we attempt to open a new one, // it will replace the existing one instead of opening an additional drawer, // since only one local drawer is supported. Therefore, layout calculations are not necessary. - if (activeDrawer && drawers?.find(drawer => drawer.id === drawerId)) { + if ((activeDrawer && drawers?.find(drawer => drawer.id === drawerId)) || activeGlobalBottomDrawerId === drawerId) { return; } // get the size of drawerId. it could be either local or global drawer @@ -117,6 +117,10 @@ export const useAppLayout = ( closeFirstDrawer(); }; + const onGlobalBottomDrawerFocus = () => { + bottomDrawersFocusControl.setFocus(); + }; + const { drawers, activeDrawer, @@ -135,7 +139,11 @@ export const useAppLayout = ( onActiveGlobalDrawersChange, expandedDrawerId, setExpandedDrawerId, - } = useDrawers({ ...rest, onGlobalDrawerFocus, onAddNewActiveDrawer }, ariaLabels, { + activeGlobalBottomDrawerId, + onActiveGlobalBottomDrawerChange, + activeGlobalBottomDrawerSize, + minGlobalBottomDrawerSize, + } = useDrawers({ ...rest, onGlobalDrawerFocus, onAddNewActiveDrawer, onGlobalBottomDrawerFocus }, ariaLabels, { ariaLabels, toolsHide, toolsOpen, @@ -222,8 +230,15 @@ export const useAppLayout = ( displayed: false, }); + const [bottomDrawerReportedSize, setBottomDrawerReportedSize] = useState(0); + const globalDrawersFocusControl = useMultipleFocusControl(true, activeGlobalDrawersIds); const drawersFocusControl = useAsyncFocusControl(!!activeDrawer?.id, true, activeDrawer?.id); + const bottomDrawersFocusControl = useAsyncFocusControl( + !!activeGlobalBottomDrawerId, + true, + activeGlobalBottomDrawerId + ); const navigationFocusControl = useAsyncFocusControl(navigationOpen, navigationTriggerHide); const splitPanelFocusControl = useSplitPanelFocusControl([splitPanelPreferences, splitPanelOpen]); @@ -285,6 +300,20 @@ export const useAppLayout = ( useGlobalScrollPadding(verticalOffsets.header ?? 0); + const getMaxGlobalBottomDrawerHeight = useCallback(() => { + const splitPanelSize = splitPanelOpen && splitPanelPosition === 'bottom' ? splitPanelReportedSize : 0; + const availableHeight = + document.documentElement.clientHeight - placement.insetBlockStart - placement.insetBlockEnd - splitPanelSize; + + // skip reading sizes in JSDOM + if (availableHeight === 0) { + return Infinity; + } + + // If the page is likely zoomed in at 200%, allow the split panel to fill the content area. + return availableHeight < 400 ? availableHeight - 40 : availableHeight - 250; + }, [splitPanelOpen, splitPanelPosition, splitPanelReportedSize, placement.insetBlockStart, placement.insetBlockEnd]); + const appLayoutInternals: AppLayoutInternals = { ariaLabels: ariaLabelsWithDrawers, headerVariant, @@ -309,6 +338,7 @@ export const useAppLayout = ( onActiveGlobalDrawersChange, drawersFocusControl, globalDrawersFocusControl, + bottomDrawersFocusControl, splitPanelPosition, splitPanelToggleConfig, splitPanelOpen, @@ -337,16 +367,27 @@ export const useAppLayout = ( maxAiDrawerSize, aiDrawerFocusControl, onActiveAiDrawerResize, + activeGlobalBottomDrawerId, + onActiveGlobalBottomDrawerChange, + activeGlobalBottomDrawerSize, + bottomDrawerReportedSize, + minGlobalBottomDrawerSize, + getMaxGlobalBottomDrawerHeight, + reportBottomDrawerSize: useStableCallback(size => setBottomDrawerReportedSize(size)), }; const splitPanelInternals: SplitPanelProviderProps = { bottomOffset: 0, - getMaxHeight: useStableCallback(() => { + getMaxHeight: useCallback(() => { + const bottomDrawerHeight = activeGlobalBottomDrawerId ? bottomDrawerReportedSize : 0; const availableHeight = - document.documentElement.clientHeight - placement.insetBlockStart - placement.insetBlockEnd; + document.documentElement.clientHeight - + placement.insetBlockStart - + placement.insetBlockEnd - + bottomDrawerHeight; // If the page is likely zoomed in at 200%, allow the split panel to fill the content area. return availableHeight < 400 ? availableHeight - 40 : availableHeight - 250; - }), + }, [activeGlobalBottomDrawerId, bottomDrawerReportedSize, placement.insetBlockEnd, placement.insetBlockStart]), maxWidth: maxSplitPanelSize, isForcedPosition: splitPanelForcedPosition, isOpen: splitPanelOpen, @@ -366,7 +407,10 @@ export const useAppLayout = ( }; const closeFirstDrawer = useStableCallback(() => { - const drawerToClose = drawersOpenQueue[drawersOpenQueue.length - 1]; + let drawerToClose = drawersOpenQueue[drawersOpenQueue.length - 1]; + if (drawerToClose === activeGlobalBottomDrawerId) { + drawerToClose = drawersOpenQueue[drawersOpenQueue.length - 2]; + } if (activeDrawer && activeDrawer?.id === drawerToClose) { onActiveDrawerChange(null, { initiatedByUserAction: true }); } else if (activeGlobalDrawersIds.includes(drawerToClose)) { diff --git a/src/app-layout/visual-refresh-toolbar/state/use-skeleton-slots-attributes.ts b/src/app-layout/visual-refresh-toolbar/state/use-skeleton-slots-attributes.ts index 4268a9ffab..56fc1237e0 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-skeleton-slots-attributes.ts +++ b/src/app-layout/visual-refresh-toolbar/state/use-skeleton-slots-attributes.ts @@ -27,6 +27,7 @@ export const useSkeletonSlotsAttributes = ( activeDrawer, expandedDrawerId, activeAiDrawer, + activeGlobalBottomDrawerId, } = appLayoutState.widgetizedState ?? {}; const { contentType, placement, maxContentWidth, navigationWidth, minContentWidth, disableContentPaddings } = appLayoutProps; @@ -34,6 +35,7 @@ export const useSkeletonSlotsAttributes = ( const toolsOpen = !!activeDrawer; const drawerExpandedMode = !!expandedDrawerId; const aiDrawerExpandedMode = expandedDrawerId === activeAiDrawer?.id; + const bottomDrawerExpandedMode = expandedDrawerId === activeGlobalBottomDrawerId; const anyPanelOpen = navigationOpen || toolsOpen; const isMaxWidth = maxContentWidth === Number.MAX_VALUE || maxContentWidth === Number.MAX_SAFE_INTEGER; @@ -43,6 +45,7 @@ export const useSkeletonSlotsAttributes = ( [styles['has-adaptive-widths-dashboard']]: contentType === 'dashboard', [styles['drawer-expanded-mode']]: drawerExpandedMode, [styles['ai-drawer-expanded-mode']]: aiDrawerExpandedMode, + [styles['bottom-drawer-expanded-mode']]: bottomDrawerExpandedMode, }), style: { minBlockSize: isNested ? '100%' : `calc(100vh - ${placement.insetBlockStart + placement.insetBlockEnd}px)`, diff --git a/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx b/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx index c618e4b53a..b59e4ab277 100644 --- a/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx +++ b/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx @@ -11,6 +11,7 @@ import OverflowMenu from '../../drawer/overflow-menu'; import { AppLayoutProps, AppLayoutPropsWithDefaults } from '../../interfaces'; import { OnChangeParams, TOOLS_DRAWER_ID } from '../../utils/use-drawers'; import { Focusable, FocusControlMultipleStates } from '../../utils/use-focus-control'; +import { InternalDrawer } from '../interfaces'; import TriggerButton from './trigger-button'; import splitPanelTestUtilStyles from '../../../split-panel/test-classes/styles.css.js'; @@ -24,21 +25,23 @@ export interface SplitPanelToggleProps { active: boolean | undefined; position: AppLayoutProps.SplitPanelPosition; } - interface DrawerTriggersProps { ariaLabels: AppLayoutPropsWithDefaults['ariaLabels']; activeDrawerId: string | null; drawersFocusRef: React.Ref | undefined; - drawers: ReadonlyArray; + drawers: ReadonlyArray; onActiveDrawerChange: ((drawerId: string | null, params: OnChangeParams) => void) | undefined; activeGlobalDrawersIds: ReadonlyArray; globalDrawersFocusControl?: FocusControlMultipleStates; - globalDrawers: ReadonlyArray; + bottomDrawersFocusRef?: React.Ref | undefined; + globalDrawers: ReadonlyArray; onActiveGlobalDrawersChange?: (newDrawerId: string, params: OnChangeParams) => void; expandedDrawerId?: string | null; setExpandedDrawerId: (value: string | null) => void; + activeGlobalBottomDrawerId?: string | null; + onActiveGlobalBottomDrawerChange?: (value: string | null, params: OnChangeParams) => void; splitPanelOpen?: boolean; splitPanelPosition?: AppLayoutProps.SplitPanelPreferences['position']; @@ -66,10 +69,14 @@ export function DrawerTriggers({ onActiveGlobalDrawersChange, expandedDrawerId, setExpandedDrawerId, + activeGlobalBottomDrawerId, + onActiveGlobalBottomDrawerChange, + bottomDrawersFocusRef, }: DrawerTriggersProps) { const isMobile = useMobile(); const hasMultipleTriggers = drawers.length > 1; const previousActiveLocalDrawerId = useRef(activeDrawerId); + const previousActiveGlobalBottomDrawerId = useRef(activeGlobalBottomDrawerId); const previousActiveGlobalDrawersIds = useRef(activeGlobalDrawersIds); const [containerWidth, triggersContainerRef] = useContainerQuery(rect => rect.contentBoxWidth); if (!drawers.length && !globalDrawers.length && !splitPanelToggleProps) { @@ -84,6 +91,10 @@ export function DrawerTriggers({ previousActiveGlobalDrawersIds.current = activeGlobalDrawersIds; } + if (activeGlobalBottomDrawerId) { + previousActiveGlobalBottomDrawerId.current = activeGlobalBottomDrawerId; + } + const getIndexOfOverflowItem = () => { if (isMobile) { return 2; @@ -204,9 +215,15 @@ export function DrawerTriggers({
)} {visibleItems.slice(globalDrawersStartIndex).map(item => { - const isForPreviousActiveDrawer = previousActiveGlobalDrawersIds?.current.includes(item.id); - const selected = + let isForPreviousActiveDrawer = previousActiveGlobalDrawersIds?.current.includes(item.id); + const isBottom = item.position === 'bottom'; + let selected = activeGlobalDrawersIds.includes(item.id) && (!expandedDrawerId || item.id === expandedDrawerId); + if (isBottom) { + selected = item.id === activeGlobalBottomDrawerId && (!expandedDrawerId || item.id === expandedDrawerId); + isForPreviousActiveDrawer = previousActiveGlobalBottomDrawerId.current === item.id; + } + return ( 0 && ( ({ - ...item, - active: activeGlobalDrawersIds.includes(item.id) && (!expandedDrawerId || item.id === expandedDrawerId), - }))} + items={overflowItems.map(item => { + const isBottom = item?.position === 'bottom'; + let active = + activeGlobalDrawersIds.includes(item.id) && (!expandedDrawerId || item.id === expandedDrawerId); + if (isBottom) { + active = item.id === activeGlobalBottomDrawerId && (!expandedDrawerId || item.id === expandedDrawerId); + } + return { + ...item, + active, + }; + })} ariaLabel={overflowMenuHasBadge ? ariaLabels?.drawersOverflowWithBadge : ariaLabels?.drawersOverflow} customTriggerBuilder={({ onClick, triggerRef, ariaLabel, ariaExpanded, testUtilsClass }) => { return ( @@ -269,6 +298,14 @@ export function DrawerTriggers({ onItemClick={event => { const id = event.detail.id; exitExpandedMode(); + const item = overflowItems.find(item => item.id === id); + const isBottom = item?.position === 'bottom'; + if (isBottom) { + const selected = + item.id === activeGlobalBottomDrawerId && (!expandedDrawerId || item.id === expandedDrawerId); + onActiveGlobalBottomDrawerChange?.(selected ? null : item.id, { initiatedByUserAction: true }); + return; + } if (globalDrawers.find(drawer => drawer.id === id)) { if (!!expandedDrawerId && id !== expandedDrawerId && activeGlobalDrawersIds.includes(id)) { return; diff --git a/src/app-layout/visual-refresh-toolbar/toolbar/index.tsx b/src/app-layout/visual-refresh-toolbar/toolbar/index.tsx index d90c9d5f39..19b7e32b5b 100644 --- a/src/app-layout/visual-refresh-toolbar/toolbar/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/toolbar/index.tsx @@ -41,11 +41,14 @@ export interface ToolbarProps { activeDrawerId?: string | null; drawers?: ReadonlyArray; drawersFocusRef?: React.Ref; + bottomDrawersFocusRef?: React.Ref; globalDrawersFocusControl?: FocusControlMultipleStates; onActiveDrawerChange?: (drawerId: string | null, params: OnChangeParams) => void; globalDrawers?: ReadonlyArray | undefined; activeGlobalDrawersIds?: ReadonlyArray; onActiveGlobalDrawersChange?: ((drawerId: string, params: OnChangeParams) => void) | undefined; + activeGlobalBottomDrawerId?: string | null; + onActiveGlobalBottomDrawerChange?: (value: string | null, params: OnChangeParams) => void; expandedDrawerId?: string | null; setExpandedDrawerId?: (value: string | null) => void; @@ -98,6 +101,9 @@ export function AppLayoutToolbarImplementation({ expandedDrawerId, setExpandedDrawerId, aiDrawerFocusRef, + onActiveGlobalBottomDrawerChange, + activeGlobalBottomDrawerId, + bottomDrawersFocusRef, } = toolbarProps; const drawerExpandedMode = !!expandedDrawerId; const ref = useRef(null); @@ -117,6 +123,7 @@ export function AppLayoutToolbarImplementation({ (!!activeDrawerId || !!activeGlobalDrawersIds?.length || !!activeAiDrawerId || + !!activeGlobalBottomDrawerId || (!!navigationOpen && !!hasNavigation)); useEffect(() => { if (anyPanelOpenInMobile) { @@ -232,11 +239,14 @@ export function AppLayoutToolbarImplementation({ onSplitPanelToggle={onSplitPanelToggle} disabled={anyPanelOpenInMobile} globalDrawersFocusControl={globalDrawersFocusControl} + bottomDrawersFocusRef={bottomDrawersFocusRef} globalDrawers={globalDrawers?.filter(item => !!item.trigger) ?? []} activeGlobalDrawersIds={activeGlobalDrawersIds ?? []} onActiveGlobalDrawersChange={onActiveGlobalDrawersChange} expandedDrawerId={expandedDrawerId} setExpandedDrawerId={setExpandedDrawerId!} + onActiveGlobalBottomDrawerChange={onActiveGlobalBottomDrawerChange} + activeGlobalBottomDrawerId={activeGlobalBottomDrawerId} />
)} diff --git a/src/app-layout/visual-refresh-toolbar/widget-areas/after-main-slot.tsx b/src/app-layout/visual-refresh-toolbar/widget-areas/after-main-slot.tsx index 22abb03a9b..cc1f0d1c81 100644 --- a/src/app-layout/visual-refresh-toolbar/widget-areas/after-main-slot.tsx +++ b/src/app-layout/visual-refresh-toolbar/widget-areas/after-main-slot.tsx @@ -9,6 +9,7 @@ import { AppLayoutDrawerImplementation as AppLayoutDrawer, AppLayoutGlobalDrawersImplementation as AppLayoutGlobalDrawers, } from '../drawer'; +import { AppLayoutBottomDrawerWrapper } from '../drawer/global-bottom-drawer'; import { SkeletonPartProps } from '../skeleton/interfaces'; import { AppLayoutSplitPanelDrawerSideImplementation as AppLayoutSplitPanelSide } from '../split-panel'; import { isWidgetReady } from '../state/invariants'; @@ -27,13 +28,24 @@ export const AfterMainSlotImplementation = ({ appLayoutState, appLayoutProps }: activeDrawer, splitPanelOpen, drawers, + globalDrawers, splitPanelPosition, + activeGlobalBottomDrawerId, } = appLayoutState.widgetizedState; const drawerExpandedMode = !!expandedDrawerId; const toolsOpen = !!activeDrawer; const globalToolsOpen = !!activeGlobalDrawersIds?.length; + const bottomDrawers = globalDrawers.filter(drawer => drawer.position === 'bottom'); + return ( <> + {!!bottomDrawers.length && ( +
+ + + +
+ )} {splitPanelPosition === 'side' && (
{ if (!isWidgetReady(appLayoutState)) { return null; } - const { splitPanelPosition, placement } = appLayoutState.widgetizedState; + const { splitPanelPosition, placement, activeGlobalBottomDrawerId, bottomDrawerReportedSize, isMobile } = + appLayoutState.widgetizedState; return ( <> {splitPanelPosition === 'bottom' && ( -
+