From 44c389912e9d859902e8ddabe4670285d4e8aaf5 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Fri, 13 Jun 2025 18:38:46 +0700 Subject: [PATCH 01/12] feat: Basic layout for a bottom global panel --- .../visual-refresh-toolbar/skeleton/index.tsx | 15 ++++++++++++++- .../skeleton/styles.scss | 19 +++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index c11e14d3d6..673b92aea4 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -46,6 +46,8 @@ interface SkeletonLayoutProps drawerExpandedModeInChildLayout: boolean; } +const BOTTOM_GLOBAL_DRAWER_HEIGHT = 200; + export const SkeletonLayout = React.forwardRef( ( { @@ -134,7 +136,10 @@ export const SkeletonLayout = React.forwardRef{content} {bottomSplitPanel && ( -
+
{bottomSplitPanel}
)} @@ -163,6 +168,14 @@ export const SkeletonLayout = React.forwardRef
{globalTools}
+
+ global tools bottom +
); diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss index a2997c154c..62a79780d3 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss @@ -50,9 +50,11 @@ // desktop grid @include desktop-only { grid-template-areas: - 'toolbar toolbar toolbar toolbar toolbar toolbar toolbar' - 'navigation . notifications . sideSplitPanel tools global-tools' - 'navigation . main . sideSplitPanel tools global-tools'; + 'toolbar toolbar toolbar toolbar toolbar toolbar toolbar' + 'navigation . notifications . sideSplitPanel tools global-tools' + 'navigation . main . sideSplitPanel tools global-tools' + 'global-tools-bottom global-tools-bottom global-tools-bottom global-tools-bottom global-tools-bottom global-tools-bottom global-tools'; + grid-template-columns: min-content minmax(#{awsui.$space-layout-content-horizontal}, 1fr) @@ -60,7 +62,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 auto; &.has-adaptive-widths-default { #{custom-props.$maxContentWidth}: map.get(constants.$adaptive-content-widths, styles.$breakpoint-xx-large); @@ -100,6 +102,15 @@ } } +.global-tools-bottom { + grid-area: global-tools-bottom; + border-block-start: 1px solid black; + position: sticky; + inset-block-end: 0; + z-index: 855; + background: #ffffff; +} + .navigation { z-index: constants.$drawer-z-index; From e7211e3b338c4498be35cf9d773a5724acccb4b6 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Mon, 30 Jun 2025 11:41:36 +0200 Subject: [PATCH 02/12] feat: Introduce positional params for runtime drawers --- pages/app-layout/utils/external-widget.tsx | 3 +++ src/app-layout/visual-refresh-toolbar/interfaces.ts | 5 +++++ src/internal/plugins/controllers/drawers.ts | 9 +++++++++ 3 files changed, 17 insertions(+) diff --git a/pages/app-layout/utils/external-widget.tsx b/pages/app-layout/utils/external-widget.tsx index 1fd2d3fa2b..894962f1c6 100644 --- a/pages/app-layout/utils/external-widget.tsx +++ b/pages/app-layout/utils/external-widget.tsx @@ -135,6 +135,9 @@ awsuiPlugins.appLayout.registerDrawer({ isExpandable: true, + movable: true, + position: 'bottom', + ariaLabels: { closeButton: 'Close button', content: 'Content', diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index 0de8881fed..48ac453e9a 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -5,6 +5,8 @@ import React from 'react'; import { BreadcrumbGroupProps } from '../../breadcrumb-group/interfaces'; import { SplitPanelSideToggleProps } from '../../internal/context/split-panel-context'; +import { NonCancelableEventHandler } from '../../internal/events'; +import { DrawerPosition, DrawerPositionChangeParams } from '../../internal/plugins/controllers/drawers'; import { AppLayoutProps, AppLayoutPropsWithDefaults } from '../interfaces'; import { OnChangeParams } from '../utils/use-drawers'; import { FocusControlMultipleStates, FocusControlState } from '../utils/use-focus-control'; @@ -19,6 +21,9 @@ export type InternalDrawer = AppLayoutProps.Drawer & { defaultActive?: boolean; isExpandable?: boolean; ariaLabels: AppLayoutProps.Drawer['ariaLabels'] & { expandedModeButton?: string }; + movable?: boolean; + position?: DrawerPosition; + onPositionChange?: NonCancelableEventHandler; }; // Widgetization notice: structures in this file are shared multiple app layout instances, possibly different minor versions. diff --git a/src/internal/plugins/controllers/drawers.ts b/src/internal/plugins/controllers/drawers.ts index d30706d411..59a7185d61 100644 --- a/src/internal/plugins/controllers/drawers.ts +++ b/src/internal/plugins/controllers/drawers.ts @@ -6,6 +6,8 @@ import { reportRuntimeApiWarning } from '../helpers/metrics'; type DrawerVisibilityChange = (callback: (isVisible: boolean) => void) => void; +export type DrawerPosition = 'side' | 'bottom'; + interface MountContentContext { onVisibilityChange: DrawerVisibilityChange; } @@ -15,6 +17,10 @@ export interface DrawerStateChangeParams { initiatedByUserAction?: boolean; } +export interface DrawerPositionChangeParams { + position: DrawerPosition; +} + export interface DrawerConfig { id: string; type?: 'local' | 'global'; @@ -40,6 +46,9 @@ export interface DrawerConfig { unmountContent: (container: HTMLElement) => void; preserveInactiveContent?: boolean; onToggle?: NonCancelableEventHandler; + movable?: boolean; + position?: DrawerPosition; + onPositionChange?: NonCancelableEventHandler; } const updatableProperties = [ From 9df1e9034351bfa4182e030eebae0225621262eb Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 1 Jul 2025 12:56:26 +0200 Subject: [PATCH 03/12] feat: Bottom drawer styles and positioning --- src/app-layout/utils/use-drawers.ts | 2 +- .../drawer/global-drawer.tsx | 33 ++++++++--- .../visual-refresh-toolbar/drawer/styles.scss | 58 +++++++++++++++++-- .../drawer/use-resize.ts | 5 +- .../visual-refresh-toolbar/skeleton/index.tsx | 10 +--- .../skeleton/styles.scss | 3 +- 6 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/app-layout/utils/use-drawers.ts b/src/app-layout/utils/use-drawers.ts index d5201cf6a8..4cac7c72fe 100644 --- a/src/app-layout/utils/use-drawers.ts +++ b/src/app-layout/utils/use-drawers.ts @@ -50,7 +50,7 @@ function getToolsDrawerItem(props: ToolsProps): AppLayoutProps.Drawer | null { }; } -const DRAWERS_LIMIT = 2; +const DRAWERS_LIMIT = 3; const DEFAULT_ON_CHANGE_PARAMS = { initiatedByUserAction: true }; diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx index 10c29fc66d..a4efac8f3d 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx @@ -57,6 +57,7 @@ function AppLayoutGlobalDrawerImplementation({ const minDrawerSize = (activeDrawerId ? minGlobalDrawersSizes[activeDrawerId] : 0) ?? 0; const maxDrawerSize = (activeDrawerId ? maxGlobalDrawersSizes[activeDrawerId] : 0) ?? 0; const refs = globalDrawersFocusControl.refs[activeDrawerId]; + const position = activeGlobalDrawer?.position ?? 'side'; const resizeProps = useResize({ currentWidth: activeDrawerSize, minWidth: minDrawerSize, @@ -64,6 +65,7 @@ function AppLayoutGlobalDrawerImplementation({ panelRef: drawerRef, handleRef: refs?.slider, onResize: size => onActiveDrawerResize({ id: activeDrawerId!, size }), + position, }); const size = getLimitedValue(minDrawerSize, activeDrawerSize, maxDrawerSize); const lastOpenedDrawerId = drawersOpenQueue.length ? drawersOpenQueue[0] : null; @@ -73,6 +75,10 @@ function AppLayoutGlobalDrawerImplementation({ const animationDisabled = (activeGlobalDrawer?.defaultActive && !drawersOpenQueue.includes(activeGlobalDrawer.id)) || (wasExpanded && !isExpanded); + const motionClassName = `with-motion-${position === 'side' ? 'horizontal' : 'vertical'}`; + const sideDrawersTotalSize = Object.entries(activeGlobalDrawersSizes) + .filter(([drawerId]) => drawerId !== activeGlobalDrawer?.id) + .reduce((acc, [, size]) => acc + size, 0); return ( @@ -86,8 +92,9 @@ function AppLayoutGlobalDrawerImplementation({ styles.drawer, styles['drawer-global'], styles[state], - !animationDisabled && sharedStyles['with-motion-horizontal'], + !animationDisabled && sharedStyles[motionClassName], !animationDisabled && isExpanded && styles['with-expanded-motion'], + styles[position], { [styles['drawer-hidden']]: !show, [styles['last-opened']]: lastOpenedDrawerId === activeDrawerId || isExpanded, @@ -114,21 +121,26 @@ function AppLayoutGlobalDrawerImplementation({ } }} style={{ - blockSize: drawerHeight, - insetBlockStart: drawerTopOffset, ...(!isMobile && { [customCssProps.drawerSize]: `${['entering', 'entered'].includes(state) ? (isExpanded ? '100%' : size + 'px') : 0}`, }), + ...(position === 'side' && { + insetBlockStart: drawerTopOffset, + blockSize: drawerHeight, + }), + ...(position === 'bottom' && { + right: sideDrawersTotalSize + 'px', + }), }} data-testid={`awsui-app-layout-drawer-${activeDrawerId}`} >
- {!isMobile &&
} + {!isMobile &&
} {!isMobile && activeGlobalDrawer?.resizable && !isExpanded && (
@@ -174,7 +186,14 @@ function AppLayoutGlobalDrawerImplementation({ />
-
+
{activeGlobalDrawer?.content}
diff --git a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss index 7b6e89d191..a61704a604 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss @@ -47,9 +47,13 @@ $drawer-resize-handle-size: awsui.$space-m; } @include desktop-only { - &:not(.legacy) { + &.side:not(.legacy) { border-inline-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; } + + &.bottom:not(.legacy) { + border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; + } } @include mobile-only { @@ -171,13 +175,25 @@ $drawer-resize-handle-size: awsui.$space-m; } > .drawer-gap { - grid-column: 1; - grid-row: 1; - block-size: 100%; - inline-size: $global-drawer-gap-size; background: awsui.$color-gap-global-drawer; - border-inline-end: awsui.$border-divider-section-width solid awsui.$color-border-layout; box-sizing: border-box; + + &.side { + grid-column: 1; + grid-row: 1; + block-size: 100%; + inline-size: $global-drawer-gap-size; + border-inline-end: awsui.$border-divider-section-width solid awsui.$color-border-layout; + } + + &.bottom { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + inline-size: 100%; + block-size: $global-drawer-gap-size; + border-block-end: awsui.$border-divider-section-width solid awsui.$color-border-layout; + } } > .drawer-slider { @@ -237,4 +253,34 @@ $drawer-resize-handle-size: awsui.$space-m; } } } + + &.bottom { + position: fixed; + inset-block-end: 0; + inset-inline-start: 0; + inline-size: auto; + overflow: auto; + block-size: var(#{custom-props.$drawerSize}); + z-index: 840; + + > .global-drawer-wrapper { + display: block; + overflow-y: auto; + position: relative; + + > .drawer-slider { + position: absolute; + inset-block-start: $global-drawer-gap-size; + inset-inline-start: 0; + inline-size: 100%; + display: flex; + justify-content: center; + z-index: 2; + } + + > .drawer-content-container { + block-size: var(#{custom-props.$drawerSize}); + } + } + } } 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 04fa41012b..7f382980fd 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/use-resize.ts +++ b/src/app-layout/visual-refresh-toolbar/drawer/use-resize.ts @@ -14,9 +14,10 @@ interface ResizeProps { panelRef: React.RefObject; handleRef: React.RefObject; onResize: (newWidth: number) => void; + position?: 'side' | 'bottom'; } -export function useResize({ currentWidth, minWidth, maxWidth, panelRef, handleRef, onResize }: ResizeProps) { +export function useResize({ currentWidth, minWidth, maxWidth, panelRef, handleRef, onResize, position }: ResizeProps) { const onResizeHandler = (newWidth: number) => { const size = getLimitedValue(minWidth, newWidth, maxWidth); @@ -26,7 +27,7 @@ export function useResize({ currentWidth, minWidth, maxWidth, panelRef, handleRe }; const sizeControlProps: SizeControlProps = { - position: 'side', + position: position ?? 'side', panelRef, handleRef, onResize: onResizeHandler, diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index 6a9ace0fb0..d62e3ce0bd 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -49,7 +49,7 @@ interface SkeletonLayoutProps drawerExpandedModeInChildLayout: boolean; } -const BOTTOM_GLOBAL_DRAWER_HEIGHT = 200; +const BOTTOM_GLOBAL_DRAWER_HEIGHT = 0; const componentAnalyticsMetadata: GeneratedAnalyticsMetadataAppLayoutToolbarComponent = { name: 'awsui.AppLayoutToolbar', @@ -180,14 +180,6 @@ export const SkeletonLayout = React.forwardRef
{globalTools}
-
- global tools bottom -
); diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss index 62a79780d3..d2f8c6fca1 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss @@ -104,11 +104,10 @@ .global-tools-bottom { grid-area: global-tools-bottom; - border-block-start: 1px solid black; position: sticky; inset-block-end: 0; z-index: 855; - background: #ffffff; + overflow: hidden; } .navigation { From fdfcd24b98fa4e81e7a1b8460aec50635dd3405b Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 1 Jul 2025 13:31:16 +0200 Subject: [PATCH 04/12] chore: Bottom drawer border --- src/app-layout/visual-refresh-toolbar/drawer/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss index a61704a604..35e91999a1 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss @@ -280,6 +280,7 @@ $drawer-resize-handle-size: awsui.$space-m; > .drawer-content-container { block-size: var(#{custom-props.$drawerSize}); + border-inline-end: awsui.$border-divider-section-width solid awsui.$color-border-layout; } } } From 9d6e3138e6dccf819ae8bf90bd8ddd959aee4ea8 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 2 Jul 2025 12:04:33 +0200 Subject: [PATCH 05/12] chore: Bottom drawer size mirroring --- src/app-layout/runtime-drawer/index.tsx | 5 +++++ .../drawer/global-drawer.tsx | 6 ------ .../visual-refresh-toolbar/drawer/styles.scss | 5 ++--- .../visual-refresh-toolbar/index.tsx | 15 ++++++++++++++- .../visual-refresh-toolbar/skeleton/index.tsx | 18 ++++++++++++++---- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/app-layout/runtime-drawer/index.tsx b/src/app-layout/runtime-drawer/index.tsx index 14004148a6..e1b3b14604 100644 --- a/src/app-layout/runtime-drawer/index.tsx +++ b/src/app-layout/runtime-drawer/index.tsx @@ -5,6 +5,8 @@ import React, { useContext, useEffect, useRef } from 'react'; import { fireNonCancelableEvent, NonCancelableEventHandler } from '../../internal/events'; import { DrawerConfig as RuntimeDrawerConfig, + DrawerPosition, + DrawerPositionChangeParams, DrawerStateChangeParams, } from '../../internal/plugins/controllers/drawers'; import { sortByPriority } from '../../internal/plugins/helpers/utils'; @@ -15,6 +17,9 @@ import styles from './styles.css.js'; export interface RuntimeDrawer extends AppLayoutProps.Drawer { onToggle?: NonCancelableEventHandler; + movable?: boolean; + position?: DrawerPosition; + onPositionChange?: NonCancelableEventHandler; } export interface DrawersLayout { diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx index a4efac8f3d..7e80beff54 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx @@ -76,9 +76,6 @@ function AppLayoutGlobalDrawerImplementation({ (activeGlobalDrawer?.defaultActive && !drawersOpenQueue.includes(activeGlobalDrawer.id)) || (wasExpanded && !isExpanded); const motionClassName = `with-motion-${position === 'side' ? 'horizontal' : 'vertical'}`; - const sideDrawersTotalSize = Object.entries(activeGlobalDrawersSizes) - .filter(([drawerId]) => drawerId !== activeGlobalDrawer?.id) - .reduce((acc, [, size]) => acc + size, 0); return ( @@ -128,9 +125,6 @@ function AppLayoutGlobalDrawerImplementation({ insetBlockStart: drawerTopOffset, blockSize: drawerHeight, }), - ...(position === 'bottom' && { - right: sideDrawersTotalSize + 'px', - }), }} data-testid={`awsui-app-layout-drawer-${activeDrawerId}`} > diff --git a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss index 35e91999a1..db19e064ac 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss @@ -47,7 +47,7 @@ $drawer-resize-handle-size: awsui.$space-m; } @include desktop-only { - &.side:not(.legacy) { + &:not(.bottom):not(.legacy) { border-inline-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; } @@ -258,7 +258,7 @@ $drawer-resize-handle-size: awsui.$space-m; position: fixed; inset-block-end: 0; inset-inline-start: 0; - inline-size: auto; + inline-size: 100%; overflow: auto; block-size: var(#{custom-props.$drawerSize}); z-index: 840; @@ -280,7 +280,6 @@ $drawer-resize-handle-size: awsui.$space-m; > .drawer-content-container { block-size: var(#{custom-props.$drawerSize}); - border-inline-end: awsui.$border-divider-section-width solid awsui.$color-border-layout; } } } diff --git a/src/app-layout/visual-refresh-toolbar/index.tsx b/src/app-layout/visual-refresh-toolbar/index.tsx index b78bfa65b0..b75673831e 100644 --- a/src/app-layout/visual-refresh-toolbar/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/index.tsx @@ -243,6 +243,9 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef navigationFocusControl.setFocus(true), })); + const activeBottomDrawerId = activeGlobalDrawers.find(drawer => drawer.position === 'bottom')?.id; + const activeBottomDrawerSize = activeBottomDrawerId ? activeGlobalDrawersSizes[activeBottomDrawerId] : 0; + const resolvedStickyNotifications = !!stickyNotifications && !isMobile; //navigation must be null if hidden so toolbar knows to hide the toggle button const resolvedNavigation = navigationHide ? null : navigation || <>; @@ -265,7 +268,15 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef key !== activeBottomDrawerId) + .reduce( + (acc, curr) => ({ + ...acc, + [curr]: activeGlobalDrawersSizes[curr], + }), + {} + ), }); const { ref: intersectionObserverRef, isIntersecting } = useIntersectionObserver({ initialState: true }); @@ -553,6 +564,8 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef ); diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index d62e3ce0bd..8cc1739756 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -1,8 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useRef } from 'react'; import clsx from 'clsx'; +import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal'; import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; import { GeneratedAnalyticsMetadataAppLayoutToolbarComponent } from '../../../app-layout-toolbar/analytics-metadata/interfaces'; @@ -47,10 +48,10 @@ interface SkeletonLayoutProps isNested?: boolean; drawerExpandedMode: boolean; drawerExpandedModeInChildLayout: boolean; + activeBottomDrawerSize: number; + activeBottomDrawerId?: string; } -const BOTTOM_GLOBAL_DRAWER_HEIGHT = 0; - const componentAnalyticsMetadata: GeneratedAnalyticsMetadataAppLayoutToolbarComponent = { name: 'awsui.AppLayoutToolbar', label: { @@ -87,12 +88,20 @@ export const SkeletonLayout = React.forwardRef { const isMobile = useMobile(); const isMaxWidth = maxContentWidth === Number.MAX_VALUE || maxContentWidth === Number.MAX_SAFE_INTEGER; const anyPanelOpen = navigationOpen || toolsOpen; + const bottomDrawerWrapperRef = useRef(null); + useResizeObserver(bottomDrawerWrapperRef, entry => { + if (activeBottomDrawerId) { + document.getElementById(activeBottomDrawerId)!.style.inlineSize = `${entry.contentBoxWidth}px`; + } + }); return (
{bottomSplitPanel}
@@ -180,6 +189,7 @@ export const SkeletonLayout = React.forwardRef
{globalTools}
+
); From fa959813d2055757d532bbbb5341250c54756fdb Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 2 Jul 2025 12:06:15 +0200 Subject: [PATCH 06/12] chore: Small refactoring --- src/app-layout/visual-refresh-toolbar/skeleton/index.tsx | 2 +- src/app-layout/visual-refresh-toolbar/skeleton/styles.scss | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index 8cc1739756..647f9d8dd9 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -189,7 +189,7 @@ export const SkeletonLayout = React.forwardRef
{globalTools}
-
+
); diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss index d2f8c6fca1..7cb302b748 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss @@ -102,11 +102,10 @@ } } -.global-tools-bottom { +.global-tools-bottom-stub { grid-area: global-tools-bottom; position: sticky; inset-block-end: 0; - z-index: 855; overflow: hidden; } From 34699aea8bfa94b00f271d7b19b05e016d47a3af Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 2 Jul 2025 13:21:45 +0200 Subject: [PATCH 07/12] feat: Bottom drawer height resizing --- .../drawer/global-drawer.tsx | 15 ++++++++++++--- .../visual-refresh-toolbar/skeleton/index.tsx | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx index 7e80beff54..bc20d10901 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx @@ -52,12 +52,21 @@ function AppLayoutGlobalDrawerImplementation({ content: activeGlobalDrawer ? activeGlobalDrawer.ariaLabels?.drawerName : ariaLabels?.tools, }; + const getMaxHeight = () => { + const availableHeight = document.documentElement.clientHeight - placement.insetBlockStart - placement.insetBlockEnd; + // If the page is likely zoomed in at 200%, allow the bottom panel to fill the content area + return availableHeight < 400 ? availableHeight - 40 : availableHeight - 250; + }; + const MIN_HEIGHT = 160; + + const position = activeGlobalDrawer?.position ?? 'side'; const { drawerTopOffset, drawerHeight } = getDrawerStyles(verticalOffsets, isMobile, placement); const activeDrawerSize = (activeDrawerId ? activeGlobalDrawersSizes[activeDrawerId] : 0) ?? 0; - const minDrawerSize = (activeDrawerId ? minGlobalDrawersSizes[activeDrawerId] : 0) ?? 0; - const maxDrawerSize = (activeDrawerId ? maxGlobalDrawersSizes[activeDrawerId] : 0) ?? 0; + const minDrawerSize = + position === 'side' ? ((activeDrawerId ? minGlobalDrawersSizes[activeDrawerId] : 0) ?? 0) : MIN_HEIGHT; + const maxDrawerSize = + position === 'side' ? ((activeDrawerId ? maxGlobalDrawersSizes[activeDrawerId] : 0) ?? 0) : getMaxHeight(); const refs = globalDrawersFocusControl.refs[activeDrawerId]; - const position = activeGlobalDrawer?.position ?? 'side'; const resizeProps = useResize({ currentWidth: activeDrawerSize, minWidth: minDrawerSize, diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index 647f9d8dd9..03fb0b3669 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -99,6 +99,7 @@ export const SkeletonLayout = React.forwardRef(null); useResizeObserver(bottomDrawerWrapperRef, entry => { if (activeBottomDrawerId) { + // TODO: turn this into a global css var and apply to the drawer document.getElementById(activeBottomDrawerId)!.style.inlineSize = `${entry.contentBoxWidth}px`; } }); From 3fbe52876cfd8c79459d10e6fabf885c58ec7326 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 2 Jul 2025 13:45:00 +0200 Subject: [PATCH 08/12] fix: Resizing bug for the bottom drawer --- src/app-layout/visual-refresh-toolbar/drawer/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss index db19e064ac..adc0561121 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss @@ -267,6 +267,7 @@ $drawer-resize-handle-size: awsui.$space-m; display: block; overflow-y: auto; position: relative; + min-inline-size: 0; > .drawer-slider { position: absolute; From b7f268a7b0e0035a205602defa070b273ce63fb8 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 2 Jul 2025 14:41:16 +0200 Subject: [PATCH 09/12] feat: Push up local content above the global bottom drawer --- build-tools/utils/custom-css-properties.js | 1 + .../visual-refresh-toolbar/compute-layout.ts | 11 ++++++++--- .../visual-refresh-toolbar/drawer/global-drawer.tsx | 6 +++--- src/app-layout/visual-refresh-toolbar/index.tsx | 5 +++-- .../visual-refresh-toolbar/skeleton/index.tsx | 1 + .../visual-refresh-toolbar/skeleton/styles.scss | 2 +- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/build-tools/utils/custom-css-properties.js b/build-tools/utils/custom-css-properties.js index b212ce560f..275b186605 100644 --- a/build-tools/utils/custom-css-properties.js +++ b/build-tools/utils/custom-css-properties.js @@ -39,6 +39,7 @@ const customCssPropertiesList = [ 'toolsMaxWidth', 'toolsWidth', 'toolsAnimationStartingOpacity', + 'activeGlobalBottomDrawerHeight', // Annotation Context Custom Properties 'contentScrollMargin', // Flashbar Custom Properties diff --git a/src/app-layout/visual-refresh-toolbar/compute-layout.ts b/src/app-layout/visual-refresh-toolbar/compute-layout.ts index 387bcf3f6e..91bffc6653 100644 --- a/src/app-layout/visual-refresh-toolbar/compute-layout.ts +++ b/src/app-layout/visual-refresh-toolbar/compute-layout.ts @@ -81,6 +81,7 @@ interface VerticalLayoutInput { toolbarHeight: number; stickyNotifications: boolean; notificationsHeight: number; + activeBottomDrawerHeight: number; } export interface VerticalLayoutOutput { @@ -88,6 +89,7 @@ export interface VerticalLayoutOutput { notifications: number; header: number; drawers: number; + bottomDrawer: number; } export function computeVerticalLayout({ @@ -96,6 +98,7 @@ export function computeVerticalLayout({ toolbarHeight, stickyNotifications, notificationsHeight, + activeBottomDrawerHeight, }: VerticalLayoutInput): VerticalLayoutOutput { const toolbar = topOffset; let notifications = topOffset; @@ -110,7 +113,7 @@ export function computeVerticalLayout({ header += notificationsHeight; } - return { toolbar, notifications, header, drawers }; + return { toolbar, notifications, header, drawers, bottomDrawer: activeBottomDrawerHeight }; } interface SplitPanelOffsetInput { @@ -150,8 +153,10 @@ export function getDrawerStyles( ): { drawerTopOffset: number; drawerHeight: string; + globalDrawerHeight: string; } { const drawerTopOffset = isMobile ? verticalOffsets.toolbar : (verticalOffsets.drawers ?? placement.insetBlockStart); - const drawerHeight = `calc(100vh - ${drawerTopOffset}px - ${placement.insetBlockEnd}px)`; - return { drawerTopOffset, drawerHeight }; + const drawerHeight = `calc(100vh - ${drawerTopOffset}px - ${placement.insetBlockEnd}px - ${verticalOffsets.bottomDrawer}px)`; + const globalDrawerHeight = `calc(100vh - ${drawerTopOffset}px - ${placement.insetBlockEnd}px)`; + return { drawerTopOffset, drawerHeight, globalDrawerHeight }; } diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx index bc20d10901..2b0bad04d8 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx @@ -60,7 +60,7 @@ function AppLayoutGlobalDrawerImplementation({ const MIN_HEIGHT = 160; const position = activeGlobalDrawer?.position ?? 'side'; - const { drawerTopOffset, drawerHeight } = getDrawerStyles(verticalOffsets, isMobile, placement); + const { drawerTopOffset, globalDrawerHeight } = getDrawerStyles(verticalOffsets, isMobile, placement); const activeDrawerSize = (activeDrawerId ? activeGlobalDrawersSizes[activeDrawerId] : 0) ?? 0; const minDrawerSize = position === 'side' ? ((activeDrawerId ? minGlobalDrawersSizes[activeDrawerId] : 0) ?? 0) : MIN_HEIGHT; @@ -132,7 +132,7 @@ function AppLayoutGlobalDrawerImplementation({ }), ...(position === 'side' && { insetBlockStart: drawerTopOffset, - blockSize: drawerHeight, + blockSize: globalDrawerHeight, }), }} data-testid={`awsui-app-layout-drawer-${activeDrawerId}`} @@ -193,7 +193,7 @@ function AppLayoutGlobalDrawerImplementation({ className={styles['drawer-content']} style={{ ...(position === 'side' && { - blockSize: drawerHeight, + blockSize: globalDrawerHeight, }), }} > diff --git a/src/app-layout/visual-refresh-toolbar/index.tsx b/src/app-layout/visual-refresh-toolbar/index.tsx index b75673831e..72d20095b8 100644 --- a/src/app-layout/visual-refresh-toolbar/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/index.tsx @@ -244,7 +244,7 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef drawer.position === 'bottom')?.id; - const activeBottomDrawerSize = activeBottomDrawerId ? activeGlobalDrawersSizes[activeBottomDrawerId] : 0; + const activeBottomDrawerHeight = activeBottomDrawerId ? activeGlobalDrawersSizes[activeBottomDrawerId] : 0; const resolvedStickyNotifications = !!stickyNotifications && !isMobile; //navigation must be null if hidden so toolbar knows to hide the toggle button @@ -322,6 +322,7 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef ); diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index 03fb0b3669..c52c703426 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -117,6 +117,7 @@ export const SkeletonLayout = React.forwardRef diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss index 7cb302b748..60d142bf21 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss @@ -200,7 +200,7 @@ .main { grid-area: main; margin-block-start: awsui.$space-scaled-s; - margin-block-end: awsui.$space-layout-content-bottom; + margin-block-end: calc(#{awsui.$space-layout-content-bottom} + var(#{custom-props.$activeGlobalBottomDrawerHeight})); &-disable-paddings { margin-block: 0; From e3cab0ccb7671855cb034469890184a4e8df7de8 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 2 Jul 2025 15:24:31 +0200 Subject: [PATCH 10/12] chore: Exclude global bottom drawers from drawersOpenQueue --- src/app-layout/utils/use-drawers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app-layout/utils/use-drawers.ts b/src/app-layout/utils/use-drawers.ts index 4cac7c72fe..c59806e102 100644 --- a/src/app-layout/utils/use-drawers.ts +++ b/src/app-layout/utils/use-drawers.ts @@ -258,7 +258,11 @@ export function useDrawers( onAddNewActiveDrawer?.(drawerId); setActiveGlobalDrawersIds(currentState => [drawerId, ...currentState].slice(0, DRAWERS_LIMIT!)); onGlobalDrawerFocus?.(drawerId, true); - drawersOpenQueue.current = [drawerId, ...drawersOpenQueue.current]; + // add only side drawers to the open queue, because it's main purpose is to keep track of opened drawers and close + // the least recently opened when they cannot be horizontally accommodated on the page anymore + if (drawer?.position !== 'bottom') { + drawersOpenQueue.current = [drawerId, ...drawersOpenQueue.current]; + } fireNonCancelableEvent(drawer?.onToggle, { isOpen: true, initiatedByUserAction }); } } From ac55094f6e05cb18bb189f10c327cae4ad4d6457 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 2 Jul 2025 15:42:31 +0200 Subject: [PATCH 11/12] chore: Small refactoring --- src/app-layout/utils/use-drawers.ts | 6 +----- src/app-layout/visual-refresh-toolbar/index.tsx | 5 ++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app-layout/utils/use-drawers.ts b/src/app-layout/utils/use-drawers.ts index c59806e102..4cac7c72fe 100644 --- a/src/app-layout/utils/use-drawers.ts +++ b/src/app-layout/utils/use-drawers.ts @@ -258,11 +258,7 @@ export function useDrawers( onAddNewActiveDrawer?.(drawerId); setActiveGlobalDrawersIds(currentState => [drawerId, ...currentState].slice(0, DRAWERS_LIMIT!)); onGlobalDrawerFocus?.(drawerId, true); - // add only side drawers to the open queue, because it's main purpose is to keep track of opened drawers and close - // the least recently opened when they cannot be horizontally accommodated on the page anymore - if (drawer?.position !== 'bottom') { - drawersOpenQueue.current = [drawerId, ...drawersOpenQueue.current]; - } + drawersOpenQueue.current = [drawerId, ...drawersOpenQueue.current]; fireNonCancelableEvent(drawer?.onToggle, { isOpen: true, initiatedByUserAction }); } } diff --git a/src/app-layout/visual-refresh-toolbar/index.tsx b/src/app-layout/visual-refresh-toolbar/index.tsx index 72d20095b8..0f75672f7c 100644 --- a/src/app-layout/visual-refresh-toolbar/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/index.tsx @@ -397,7 +397,10 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef { - const drawerToClose = drawersOpenQueue[drawersOpenQueue.length - 1]; + const sideDrawersOpenQueue = drawersOpenQueue.filter( + drawerId => globalDrawers.find(globalDrawer => globalDrawer.id === drawerId)?.position !== 'bottom' + ); + const drawerToClose = sideDrawersOpenQueue[sideDrawersOpenQueue.length - 1]; if (activeDrawer && activeDrawer?.id === drawerToClose) { onActiveDrawerChange(null, { initiatedByUserAction: true }); } else if (activeGlobalDrawersIds.includes(drawerToClose)) { From 919f0690aeaf84d413d35bbb58481f2321e75b60 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 2 Jul 2025 16:06:36 +0200 Subject: [PATCH 12/12] feat: Global bottom drawer for mobile view --- .../visual-refresh-toolbar/drawer/styles.scss | 50 ++++++++++--------- .../visual-refresh-toolbar/skeleton/index.tsx | 6 ++- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss index adc0561121..fc3de2744e 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss @@ -255,32 +255,34 @@ $drawer-resize-handle-size: awsui.$space-m; } &.bottom { - position: fixed; - inset-block-end: 0; - inset-inline-start: 0; - inline-size: 100%; - overflow: auto; - block-size: var(#{custom-props.$drawerSize}); - z-index: 840; - - > .global-drawer-wrapper { - display: block; - overflow-y: auto; - position: relative; - min-inline-size: 0; + @include desktop-only { + position: fixed; + inset-block-end: 0; + inset-inline-start: 0; + inline-size: 100%; + overflow: auto; + block-size: var(#{custom-props.$drawerSize}); + z-index: 840; - > .drawer-slider { - position: absolute; - inset-block-start: $global-drawer-gap-size; - inset-inline-start: 0; - inline-size: 100%; - display: flex; - justify-content: center; - z-index: 2; - } + > .global-drawer-wrapper { + display: block; + overflow-y: auto; + position: relative; + min-inline-size: 0; + + > .drawer-slider { + position: absolute; + inset-block-start: $global-drawer-gap-size; + inset-inline-start: 0; + inline-size: 100%; + display: flex; + justify-content: center; + z-index: 2; + } - > .drawer-content-container { - block-size: var(#{custom-props.$drawerSize}); + > .drawer-content-container { + block-size: var(#{custom-props.$drawerSize}); + } } } } diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index c52c703426..9655460d88 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -100,7 +100,11 @@ export const SkeletonLayout = React.forwardRef { if (activeBottomDrawerId) { // TODO: turn this into a global css var and apply to the drawer - document.getElementById(activeBottomDrawerId)!.style.inlineSize = `${entry.contentBoxWidth}px`; + if (!isMobile) { + document.getElementById(activeBottomDrawerId)!.style.inlineSize = `${entry.contentBoxWidth}px`; + } else { + document.getElementById(activeBottomDrawerId)!.style.inlineSize = '100%'; + } } }); return (