From 26bb4d81d0cac5878b0aea3a702da8fc9866af2c Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 26 May 2026 14:42:54 +0200 Subject: [PATCH 01/13] feat: Left panel without toolbar in AppLayout component --- .../without-toolbar-nested.page.tsx | 142 ++++++++++++++++++ .../without-toolbar.page.tsx | 7 + pages/app-layout/without-toolbar.page.tsx | 122 +++++++++++++++ .../visual-refresh-toolbar/skeleton/index.tsx | 1 + .../skeleton/styles.scss | 42 ++++++ .../state/props-merger.ts | 2 +- .../state/use-app-layout.tsx | 6 +- .../state/use-skeleton-slots-attributes.ts | 2 + 8 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 pages/app-layout-toolbar/without-toolbar-nested.page.tsx create mode 100644 pages/app-layout/without-toolbar.page.tsx diff --git a/pages/app-layout-toolbar/without-toolbar-nested.page.tsx b/pages/app-layout-toolbar/without-toolbar-nested.page.tsx new file mode 100644 index 0000000000..a3da64db09 --- /dev/null +++ b/pages/app-layout-toolbar/without-toolbar-nested.page.tsx @@ -0,0 +1,142 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useRef, useState } from 'react'; + +import { AppLayoutToolbar, Button, ContentLayout, Header, HelpPanel, Link, SpaceBetween } from '~components'; +import { AppLayoutToolbarProps } from '~components/app-layout-toolbar'; +import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget'; + +import { Containers, CustomDrawerContent, Navigation } from '../app-layout/utils/content-blocks'; +import { drawerLabels } from '../app-layout/utils/drawers'; +import { leftDrawerPayload } from '../app-layout/utils/external-global-left-panel-widget'; +import appLayoutLabels from '../app-layout/utils/labels'; +import ScreenshotArea from '../utils/screenshot-area'; + +registerLeftDrawer(leftDrawerPayload); + +export default function WithDrawers() { + const [activeDrawerId, setActiveDrawerId] = useState(null); + const [helpPathSlug, setHelpPathSlug] = useState('default'); + const [isToolsOpen, setIsToolsOpen] = useState(false); + const [isNavigationOpen, setIsNavigationOpen] = useState(true); + const pageLayoutRef = useRef(null); + + const drawersProps: Pick | null = { + activeDrawerId: activeDrawerId, + drawers: [ + { + ariaLabels: { + closeButton: 'ProHelp close button', + drawerName: 'ProHelp drawer content', + triggerButton: 'ProHelp trigger button', + resizeHandle: 'ProHelp resize handle', + }, + content: , + id: 'pro-help', + }, + ], + onDrawerChange: event => { + setActiveDrawerId(event.detail.activeDrawerId); + }, + }; + + return ( + + +
{ + setHelpPathSlug('header'); + setIsToolsOpen(true); + pageLayoutRef.current?.focusToolsClose(); + }} + > + Info + + } + > + Page layout without the toolbar +
+ + + + + + + + + + } + > +
{ + setHelpPathSlug('content'); + setIsToolsOpen(true); + }} + > + Info + + } + > + Content +
+ + + } + >
+ } + onToolsChange={event => { + setIsToolsOpen(event.detail.open); + }} + tools={} + toolsOpen={isToolsOpen} + navigationTriggerHide={true} + navigationOpen={isNavigationOpen} + navigation={} + onNavigationChange={event => setIsNavigationOpen(event.detail.open)} + {...drawersProps} + /> +
+ ); +} + +function Info({ helpPathSlug }: { helpPathSlug: string }) { + return Info}>Here is some info for you: {helpPathSlug}; +} diff --git a/pages/app-layout-toolbar/without-toolbar.page.tsx b/pages/app-layout-toolbar/without-toolbar.page.tsx index 48dfbfc880..aa5657f34f 100644 --- a/pages/app-layout-toolbar/without-toolbar.page.tsx +++ b/pages/app-layout-toolbar/without-toolbar.page.tsx @@ -4,12 +4,16 @@ import React, { useRef, useState } from 'react'; import { AppLayoutToolbar, Button, ContentLayout, Header, HelpPanel, Link, SpaceBetween } from '~components'; import { AppLayoutToolbarProps } from '~components/app-layout-toolbar'; +import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget'; import { Containers, CustomDrawerContent, Navigation } from '../app-layout/utils/content-blocks'; import { drawerLabels } from '../app-layout/utils/drawers'; +import { leftDrawerPayload } from '../app-layout/utils/external-global-left-panel-widget'; import appLayoutLabels from '../app-layout/utils/labels'; import ScreenshotArea from '../utils/screenshot-area'; +registerLeftDrawer(leftDrawerPayload); + export default function WithDrawers() { const [activeDrawerId, setActiveDrawerId] = useState(null); const [helpPathSlug, setHelpPathSlug] = useState('default'); @@ -85,6 +89,9 @@ export default function WithDrawers() { Open a drawer without trigger + } diff --git a/pages/app-layout/without-toolbar.page.tsx b/pages/app-layout/without-toolbar.page.tsx new file mode 100644 index 0000000000..e7879dec97 --- /dev/null +++ b/pages/app-layout/without-toolbar.page.tsx @@ -0,0 +1,122 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useRef, useState } from 'react'; + +import { AppLayout, Button, ContentLayout, Header, HelpPanel, Link, SpaceBetween } from '~components'; +import { AppLayoutToolbarProps } from '~components/app-layout-toolbar'; +import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget'; + +import { Containers, CustomDrawerContent } from '../app-layout/utils/content-blocks'; +import { drawerLabels } from '../app-layout/utils/drawers'; +import { leftDrawerPayload } from '../app-layout/utils/external-global-left-panel-widget'; +import appLayoutLabels from '../app-layout/utils/labels'; +import ScreenshotArea from '../utils/screenshot-area'; + +registerLeftDrawer(leftDrawerPayload); + +export default function WithoutToolbarPage() { + const [activeDrawerId, setActiveDrawerId] = useState(null); + const [helpPathSlug, setHelpPathSlug] = useState('default'); + const [isToolsOpen, setIsToolsOpen] = useState(false); + const pageLayoutRef = useRef(null); + + const drawersProps: Pick | null = { + activeDrawerId: activeDrawerId, + drawers: [ + { + ariaLabels: { + closeButton: 'ProHelp close button', + drawerName: 'ProHelp drawer content', + triggerButton: 'ProHelp trigger button', + resizeHandle: 'ProHelp resize handle', + }, + content: , + id: 'pro-help', + }, + ], + onDrawerChange: event => { + setActiveDrawerId(event.detail.activeDrawerId); + }, + }; + + return ( + + +
{ + setHelpPathSlug('header'); + setIsToolsOpen(true); + pageLayoutRef.current?.focusToolsClose(); + }} + > + Info + + } + > + Page layout without the toolbar +
+ + + + + + + + } + > +
{ + setHelpPathSlug('content'); + setIsToolsOpen(true); + }} + > + Info + + } + > + Content +
+ + + } + onToolsChange={event => { + setIsToolsOpen(event.detail.open); + }} + tools={} + toolsOpen={isToolsOpen} + navigationHide={true} + {...drawersProps} + /> +
+ ); +} + +function Info({ helpPathSlug }: { helpPathSlug: string }) { + return Info}>Here is some info for you: {helpPathSlug}; +} diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index 2608d94a03..aafb93a212 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -76,6 +76,7 @@ export const SkeletonLayout = ({ clsx(styles.root, testutilStyles.root, { [styles['has-adaptive-widths-default']]: !contentTypeCustomWidths.includes(contentType), [styles['has-adaptive-widths-dashboard']]: contentType === 'dashboard', + [styles['has-no-toolbar']]: !toolbarProps, }) } style={ diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss index c0713f92bb..d36c91d403 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss @@ -65,6 +65,48 @@ min-content; grid-template-rows: min-content min-content 1fr min-content min-content; + &.has-no-toolbar { + &::before { + @include theming.dark-mode-only { + content: ''; + grid-row: 1 / -1; + grid-column: 2 / -2; + pointer-events: none; + z-index: constants.$drawer-z-index + 1; + border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; + } + } + + &.has-open-left-panel::before { + @include theming.dark-mode-only { + border-inline-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; + border-start-start-radius: awsui.$space-xxs; + } + } + + &.has-open-left-panel::after { + content: ''; + grid-row: 2; + grid-column: 2; + inline-size: 5px; + block-size: 5px; + margin-block-end: -5px; + align-self: start; + justify-self: start; + background: radial-gradient( + circle at 100% 100%, + awsui.$color-background-layout-main 70%, + constants.$ai-drawer-background 70% + ); + z-index: constants.$drawer-z-index + 1; + pointer-events: none; + + @include theming.dark-mode-only { + display: none; + } + } + } + &.has-adaptive-widths-default { #{custom-props.$maxContentWidth}: map.get(constants.$adaptive-content-widths, styles.$breakpoint-xx-large); } 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 37299c2b4a..d903f780ff 100644 --- a/src/app-layout/visual-refresh-toolbar/state/props-merger.ts +++ b/src/app-layout/visual-refresh-toolbar/state/props-merger.ts @@ -42,7 +42,7 @@ export const mergeProps: MergeProps = (ownProps, additionalProps) => { toolbar.onActiveGlobalDrawersChange = props.onActiveGlobalDrawersChange; } if ( - props.aiDrawer && + props.aiDrawer?.trigger && props.aiDrawerFocusRef && !checkAlreadyExists(!!toolbar.aiDrawerFocusRef, 'aiDrawerFocusRef') ) { 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 aafb2b36c1..07e0f0b2a6 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 @@ -74,10 +74,12 @@ export const useAppLayout = ( const [isNested, setIsNested] = useState(false); const [expandedDrawerId, setInternalExpandedDrawerId] = useState(null); const rootRefInternal = useRef(null); + const isNestedInitiated = useRef(false); // This workaround ensures the ref is defined before checking if the app layout is nested. // On initial render, the ref might be undefined because this component loads asynchronously via the widget API. const onMountRootRef = useCallback(node => { setIsNested(getIsNestedInAppLayout(node)); + isNestedInitiated.current = true; }, []); const [toolsOpen = false, setToolsOpen] = useControllable(controlledToolsOpen, onToolsChange, false, { @@ -239,7 +241,9 @@ export const useAppLayout = ( } }; - useWidgetMessages(hasToolbar, message => { + // listen widget messages only on outermost AppLayout in case they are nested + const isOutermost = !isNested && isNestedInitiated.current; + useWidgetMessages(isOutermost, message => { if (message.type === 'expandDrawer' || message.type === 'exitExpandedMode') { drawerGenericMessageHandler(message); return; 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 5a114a0f95..23f067f7f0 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 @@ -46,6 +46,8 @@ export const useSkeletonSlotsAttributes = ( [styles['drawer-expanded-mode']]: drawerExpandedMode, [styles['ai-drawer-expanded-mode']]: aiDrawerExpandedMode, [styles['bottom-drawer-expanded-mode']]: bottomDrawerExpandedMode, + [styles['has-no-toolbar']]: !hasToolbar && !isNested, + [styles['has-open-left-panel']]: !!activeAiDrawer, }), style: { minBlockSize: isNested ? '100%' : `calc(100vh - ${placement.insetBlockStart + placement.insetBlockEnd}px)`, From abd7aabe6cb18529414796bef4c37b2124866851 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 26 May 2026 18:04:16 +0200 Subject: [PATCH 02/13] chore: u tests --- src/__a11y__/a11y-app-layout-toolbar.test.ts | 1 + .../runtime-drawers-widgetized.test.tsx | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/__a11y__/a11y-app-layout-toolbar.test.ts b/src/__a11y__/a11y-app-layout-toolbar.test.ts index b2e47addb0..3658bd44ee 100644 --- a/src/__a11y__/a11y-app-layout-toolbar.test.ts +++ b/src/__a11y__/a11y-app-layout-toolbar.test.ts @@ -11,6 +11,7 @@ const EXCLUDED_PAGES = [ // Not a use case that's encouraged. 'app-layout/multi-layout-global-drawer-child-layout', 'app-layout/with-error-boundaries', + 'app-layout-toolbar/without-toolbar-nested', ]; describe('A11y checks for app layout toolbar', () => { diff --git a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx index 7c5d0e5488..542b445a07 100644 --- a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx @@ -302,6 +302,52 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, ({ size }) => { } }); + describe('left drawer without toolbar', () => { + test('should render left drawer when toolbar is not present', () => { + awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, trigger: undefined, defaultActive: true }); + const { globalDrawersWrapper, wrapper } = renderComponent(); + + expect(wrapper.findToolbar()).toBeFalsy(); + expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)!.isActive()).toBe(true); + }); + + test('should open left drawer via API when toolbar is not present', () => { + awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, trigger: undefined }); + const { globalDrawersWrapper, wrapper } = renderComponent(); + + expect(wrapper.findToolbar()).toBeFalsy(); + expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)).toBeFalsy(); + + act(() => awsuiWidgetPlugins.updateDrawer({ type: 'openDrawer', payload: { id: drawerDefaults.id } })); + + expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)!.isActive()).toBe(true); + }); + + test('should render left drawer when toolbar is not present and has a nested app layout', () => { + awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, trigger: undefined, defaultActive: true }); + const { container } = render( + } + /> + ); + + const outerWrapper = createWrapper(container).find('[data-testid="outer"]')!.findAppLayout()!; + const innerWrapper = createWrapper(container).find('[data-testid="inner"]')!.findAppLayout()!; + const outerDrawersUtils = getGlobalDrawersTestUtils(outerWrapper); + const innerDrawersUtils = getGlobalDrawersTestUtils(innerWrapper); + + expect(outerWrapper.findToolbar()).toBeFalsy(); + expect(innerWrapper.findToolbar()).toBeFalsy(); + expect(outerDrawersUtils.findDrawerById(drawerDefaults.id)!.isActive()).toBe(true); + expect(outerDrawersUtils.findActiveDrawers().length).toBe(1); + expect(innerDrawersUtils.findDrawerById(drawerDefaults.id)).toBeFalsy(); + expect(innerDrawersUtils.findActiveDrawers().length).toBe(0); + }); + }); + describe('metrics', () => { let sendPanoramaMetricSpy: jest.SpyInstance; beforeEach(() => { From 3ab8cd7f7f49f5e40d2fb49a3c9b180821b83699 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 26 May 2026 19:38:43 +0200 Subject: [PATCH 03/13] chore: skip app-layout-toolbar/without-toolbar-nested for a11y --- src/__a11y__/run-a11y-tests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__a11y__/run-a11y-tests.ts b/src/__a11y__/run-a11y-tests.ts index a7e6411493..616253c818 100644 --- a/src/__a11y__/run-a11y-tests.ts +++ b/src/__a11y__/run-a11y-tests.ts @@ -34,6 +34,7 @@ export default function runA11yTests(theme: Theme, mode: Mode, skip: string[] = // this page intentionally has issues to test the helper 'undefined-texts', 'app-layout/with-error-boundaries', + 'app-layout-toolbar/without-toolbar-nested', ]; const testFunction = skipPages.includes(inputUrl) || From 2a226613de21c13e00b1bffcd2e63143f8f30b65 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 27 May 2026 13:45:51 +0200 Subject: [PATCH 04/13] chore: Introduced a pseudo toolbar with appropriate styling --- .../skeleton/styles.scss | 100 ++++++++++-------- .../state/use-skeleton-slots-attributes.ts | 2 - .../widget-areas/before-main-slot.tsx | 10 ++ 3 files changed, 68 insertions(+), 44 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss index d36c91d403..92879161e7 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss @@ -65,48 +65,6 @@ min-content; grid-template-rows: min-content min-content 1fr min-content min-content; - &.has-no-toolbar { - &::before { - @include theming.dark-mode-only { - content: ''; - grid-row: 1 / -1; - grid-column: 2 / -2; - pointer-events: none; - z-index: constants.$drawer-z-index + 1; - border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; - } - } - - &.has-open-left-panel::before { - @include theming.dark-mode-only { - border-inline-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; - border-start-start-radius: awsui.$space-xxs; - } - } - - &.has-open-left-panel::after { - content: ''; - grid-row: 2; - grid-column: 2; - inline-size: 5px; - block-size: 5px; - margin-block-end: -5px; - align-self: start; - justify-self: start; - background: radial-gradient( - circle at 100% 100%, - awsui.$color-background-layout-main 70%, - constants.$ai-drawer-background 70% - ); - z-index: constants.$drawer-z-index + 1; - pointer-events: none; - - @include theming.dark-mode-only { - display: none; - } - } - } - &.has-adaptive-widths-default { #{custom-props.$maxContentWidth}: map.get(constants.$adaptive-content-widths, styles.$breakpoint-xx-large); } @@ -256,6 +214,64 @@ box-sizing: border-box; } +/** + * A zero-height sticky element placed in the toolbar grid area when the toolbar is absent + * and the left panel (AI drawer) is open. It reuses the toolbar's pseudo-element technique + * to render the border-radius corner between the AI drawer and the content area. + * In light mode: inverse radius trick (dark square + white square with radius). + * In dark mode: a vertical border line with a rounded top corner + top border on the element itself. + */ +.pseudo-toolbar { + grid-area: toolbar; + position: sticky; + z-index: constants.$toolbar-z-index; + + &:before { + @include theming.dark-mode-only { + content: ''; + position: absolute; + inline-size: awsui.$border-divider-section-width; + block-size: 100vh; + background: awsui.$color-border-layout; + } + } +} + +.pseudo-toolbar-content { + position: relative; + + @include theming.dark-mode-only { + border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; + border-start-start-radius: awsui.$space-xxs; + } + + &:before, + &:after { + content: ''; + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + inline-size: 5px; + block-size: 5px; + background: constants.$ai-drawer-background; + + @include theming.dark-mode-only { + inset-block-start: -1px; + } + } + + &::after { + background-color: awsui.$color-background-layout-toolbar; + border-start-start-radius: awsui.$space-xxs; + + @include theming.dark-mode-only { + background-color: transparent; + border-inline-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; + border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-layout; + } + } +} + .notifications-container { grid-area: notifications; } 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 23f067f7f0..5a114a0f95 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 @@ -46,8 +46,6 @@ export const useSkeletonSlotsAttributes = ( [styles['drawer-expanded-mode']]: drawerExpandedMode, [styles['ai-drawer-expanded-mode']]: aiDrawerExpandedMode, [styles['bottom-drawer-expanded-mode']]: bottomDrawerExpandedMode, - [styles['has-no-toolbar']]: !hasToolbar && !isNested, - [styles['has-open-left-panel']]: !!activeAiDrawer, }), style: { minBlockSize: isNested ? '100%' : `calc(100vh - ${placement.insetBlockStart + placement.insetBlockEnd}px)`, diff --git a/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx b/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx index 3ab05d0d04..e18d304b3d 100644 --- a/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx +++ b/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx @@ -68,6 +68,16 @@ export const BeforeMainSlotImplementationInternal = ({ featureNotificationsProps={featureNotificationsProps} /> )} + {!toolbarProps && !!activeAiDrawerId && ( +
+
+
+ )} {aiDrawer && (
Date: Wed, 27 May 2026 14:11:33 +0200 Subject: [PATCH 05/13] chore: Tweaked testing pages --- .../without-toolbar.page.tsx | 135 ------------------ .../without-toolbar-nested.page.tsx | 10 +- pages/app-layout/without-toolbar.page.tsx | 25 +++- 3 files changed, 24 insertions(+), 146 deletions(-) delete mode 100644 pages/app-layout-toolbar/without-toolbar.page.tsx rename pages/{app-layout-toolbar => app-layout}/without-toolbar-nested.page.tsx (93%) diff --git a/pages/app-layout-toolbar/without-toolbar.page.tsx b/pages/app-layout-toolbar/without-toolbar.page.tsx deleted file mode 100644 index aa5657f34f..0000000000 --- a/pages/app-layout-toolbar/without-toolbar.page.tsx +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React, { useRef, useState } from 'react'; - -import { AppLayoutToolbar, Button, ContentLayout, Header, HelpPanel, Link, SpaceBetween } from '~components'; -import { AppLayoutToolbarProps } from '~components/app-layout-toolbar'; -import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget'; - -import { Containers, CustomDrawerContent, Navigation } from '../app-layout/utils/content-blocks'; -import { drawerLabels } from '../app-layout/utils/drawers'; -import { leftDrawerPayload } from '../app-layout/utils/external-global-left-panel-widget'; -import appLayoutLabels from '../app-layout/utils/labels'; -import ScreenshotArea from '../utils/screenshot-area'; - -registerLeftDrawer(leftDrawerPayload); - -export default function WithDrawers() { - const [activeDrawerId, setActiveDrawerId] = useState(null); - const [helpPathSlug, setHelpPathSlug] = useState('default'); - const [isToolsOpen, setIsToolsOpen] = useState(false); - const [isNavigationOpen, setIsNavigationOpen] = useState(true); - const pageLayoutRef = useRef(null); - - const drawersProps: Pick | null = { - activeDrawerId: activeDrawerId, - drawers: [ - { - ariaLabels: { - closeButton: 'ProHelp close button', - drawerName: 'ProHelp drawer content', - triggerButton: 'ProHelp trigger button', - resizeHandle: 'ProHelp resize handle', - }, - content: , - id: 'pro-help', - }, - ], - onDrawerChange: event => { - setActiveDrawerId(event.detail.activeDrawerId); - }, - }; - - return ( - - -
{ - setHelpPathSlug('header'); - setIsToolsOpen(true); - pageLayoutRef.current?.focusToolsClose(); - }} - > - Info - - } - > - Page layout without the toolbar -
- - - - - - - - - - } - > -
{ - setHelpPathSlug('content'); - setIsToolsOpen(true); - }} - > - Info - - } - > - Content -
- - - } - onToolsChange={event => { - setIsToolsOpen(event.detail.open); - }} - tools={} - toolsOpen={isToolsOpen} - navigationTriggerHide={true} - navigationOpen={isNavigationOpen} - navigation={} - onNavigationChange={event => setIsNavigationOpen(event.detail.open)} - {...drawersProps} - /> -
- ); -} - -function Info({ helpPathSlug }: { helpPathSlug: string }) { - return Info}>Here is some info for you: {helpPathSlug}; -} diff --git a/pages/app-layout-toolbar/without-toolbar-nested.page.tsx b/pages/app-layout/without-toolbar-nested.page.tsx similarity index 93% rename from pages/app-layout-toolbar/without-toolbar-nested.page.tsx rename to pages/app-layout/without-toolbar-nested.page.tsx index a3da64db09..f968cada9c 100644 --- a/pages/app-layout-toolbar/without-toolbar-nested.page.tsx +++ b/pages/app-layout/without-toolbar-nested.page.tsx @@ -6,13 +6,13 @@ import { AppLayoutToolbar, Button, ContentLayout, Header, HelpPanel, Link, Space import { AppLayoutToolbarProps } from '~components/app-layout-toolbar'; import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget'; -import { Containers, CustomDrawerContent, Navigation } from '../app-layout/utils/content-blocks'; -import { drawerLabels } from '../app-layout/utils/drawers'; -import { leftDrawerPayload } from '../app-layout/utils/external-global-left-panel-widget'; -import appLayoutLabels from '../app-layout/utils/labels'; import ScreenshotArea from '../utils/screenshot-area'; +import { Containers, CustomDrawerContent, Navigation } from './utils/content-blocks'; +import { drawerLabels } from './utils/drawers'; +import { leftDrawerPayload } from './utils/external-global-left-panel-widget'; +import appLayoutLabels from './utils/labels'; -registerLeftDrawer(leftDrawerPayload); +registerLeftDrawer({ ...leftDrawerPayload, defaultActive: true }); export default function WithDrawers() { const [activeDrawerId, setActiveDrawerId] = useState(null); diff --git a/pages/app-layout/without-toolbar.page.tsx b/pages/app-layout/without-toolbar.page.tsx index e7879dec97..9387a3ee7f 100644 --- a/pages/app-layout/without-toolbar.page.tsx +++ b/pages/app-layout/without-toolbar.page.tsx @@ -6,18 +6,19 @@ import { AppLayout, Button, ContentLayout, Header, HelpPanel, Link, SpaceBetween import { AppLayoutToolbarProps } from '~components/app-layout-toolbar'; import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget'; -import { Containers, CustomDrawerContent } from '../app-layout/utils/content-blocks'; -import { drawerLabels } from '../app-layout/utils/drawers'; -import { leftDrawerPayload } from '../app-layout/utils/external-global-left-panel-widget'; -import appLayoutLabels from '../app-layout/utils/labels'; import ScreenshotArea from '../utils/screenshot-area'; +import { Containers, CustomDrawerContent, Navigation } from './utils/content-blocks'; +import { drawerLabels } from './utils/drawers'; +import { leftDrawerPayload } from './utils/external-global-left-panel-widget'; +import appLayoutLabels from './utils/labels'; -registerLeftDrawer(leftDrawerPayload); +registerLeftDrawer({ ...leftDrawerPayload, defaultActive: true }); -export default function WithoutToolbarPage() { +export default function WithDrawers() { const [activeDrawerId, setActiveDrawerId] = useState(null); const [helpPathSlug, setHelpPathSlug] = useState('default'); const [isToolsOpen, setIsToolsOpen] = useState(false); + const [isNavigationOpen, setIsNavigationOpen] = useState(true); const pageLayoutRef = useRef(null); const drawersProps: Pick | null = { @@ -70,6 +71,15 @@ export default function WithoutToolbarPage() { + + + + + + + + + } + > +
{ + setHelpPathSlug('content'); + setIsToolsOpen(true); + }} + > + Info + + } + > + Content +
+ + + } + /> + )} + /> + } + onToolsChange={event => { + setIsToolsOpen(event.detail.open); + }} + tools={} + toolsOpen={isToolsOpen} + navigationTriggerHide={true} + navigationOpen={isNavigationOpen} + navigation={} + onNavigationChange={event => setIsNavigationOpen(event.detail.open)} + {...drawersProps} + /> + + ); +} + +function Info({ helpPathSlug }: { helpPathSlug: string }) { + return Info}>Here is some info for you: {helpPathSlug}; +} diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx index aafb93a212..2608d94a03 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/index.tsx @@ -76,7 +76,6 @@ export const SkeletonLayout = ({ clsx(styles.root, testutilStyles.root, { [styles['has-adaptive-widths-default']]: !contentTypeCustomWidths.includes(contentType), [styles['has-adaptive-widths-dashboard']]: contentType === 'dashboard', - [styles['has-no-toolbar']]: !toolbarProps, }) } style={ From 65329a752073c03f55845ffca55e98eff6f37a5f Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 28 May 2026 10:59:14 +0200 Subject: [PATCH 09/13] chore: Introduce a flag __forceEnableRuntimeMessages to enable runtime messages without for instances without toolbar --- .../without-toolbar-nested-iframe.page.tsx | 1 + .../without-toolbar-nested.page.tsx | 1 + .../without-toolbar.page.tsx | 1 + src/__a11y__/a11y-app-layout-toolbar.test.ts | 1 + .../runtime-drawers-widgetized.test.tsx | 21 +++++++++++++++---- .../skeleton/multi-layout.ts | 6 +++--- .../state/use-app-layout.tsx | 7 ++----- 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/pages/app-layout-toolbar/without-toolbar-nested-iframe.page.tsx b/pages/app-layout-toolbar/without-toolbar-nested-iframe.page.tsx index 9ea8102117..5b87e3e661 100644 --- a/pages/app-layout-toolbar/without-toolbar-nested-iframe.page.tsx +++ b/pages/app-layout-toolbar/without-toolbar-nested-iframe.page.tsx @@ -44,6 +44,7 @@ export default function WithDrawers() { return ( { diff --git a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx index 542b445a07..a67edb7d1a 100644 --- a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx @@ -303,17 +303,29 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, ({ size }) => { }); describe('left drawer without toolbar', () => { - test('should render left drawer when toolbar is not present', () => { + test('should not render left drawer when toolbar is not present', () => { awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, trigger: undefined, defaultActive: true }); const { globalDrawersWrapper, wrapper } = renderComponent(); + expect(wrapper.findToolbar()).toBeFalsy(); + expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)).toBeFalsy(); + }); + + test('should render left drawer when toolbar is not present (__forceEnableRuntimeMessages is provided)', () => { + awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, trigger: undefined, defaultActive: true }); + const { globalDrawersWrapper, wrapper } = renderComponent( + + ); + expect(wrapper.findToolbar()).toBeFalsy(); expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)!.isActive()).toBe(true); }); - test('should open left drawer via API when toolbar is not present', () => { + test('should open left drawer via API when toolbar is not present (__forceEnableRuntimeMessages is provided)', () => { awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, trigger: undefined }); - const { globalDrawersWrapper, wrapper } = renderComponent(); + const { globalDrawersWrapper, wrapper } = renderComponent( + + ); expect(wrapper.findToolbar()).toBeFalsy(); expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)).toBeFalsy(); @@ -323,10 +335,11 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, ({ size }) => { expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)!.isActive()).toBe(true); }); - test('should render left drawer when toolbar is not present and has a nested app layout', () => { + test('should render left drawer when toolbar is not present and has a nested app layout (__forceEnableRuntimeMessages is provided)', () => { awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, trigger: undefined, defaultActive: true }); const { container } = render( - setRegistration(props as RegistrationState) - ); + const unregister = awsuiPluginsInternal.appLayoutWidget.register(forceDeduplicationType, props => { + setRegistration(props as RegistrationState); + }); return () => { unregister(); setRegistration({ type: 'suspended' }); 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 07e0f0b2a6..e94554d007 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 @@ -74,13 +74,12 @@ export const useAppLayout = ( const [isNested, setIsNested] = useState(false); const [expandedDrawerId, setInternalExpandedDrawerId] = useState(null); const rootRefInternal = useRef(null); - const isNestedInitiated = useRef(false); // This workaround ensures the ref is defined before checking if the app layout is nested. // On initial render, the ref might be undefined because this component loads asynchronously via the widget API. const onMountRootRef = useCallback(node => { setIsNested(getIsNestedInAppLayout(node)); - isNestedInitiated.current = true; }, []); + const { __forceEnableRuntimeMessages: forceEnableRuntimeMessages } = rest as any; const [toolsOpen = false, setToolsOpen] = useControllable(controlledToolsOpen, onToolsChange, false, { componentName: 'AppLayout', @@ -241,9 +240,7 @@ export const useAppLayout = ( } }; - // listen widget messages only on outermost AppLayout in case they are nested - const isOutermost = !isNested && isNestedInitiated.current; - useWidgetMessages(isOutermost, message => { + useWidgetMessages(hasToolbar || forceEnableRuntimeMessages, message => { if (message.type === 'expandDrawer' || message.type === 'exitExpandedMode') { drawerGenericMessageHandler(message); return; From 8784e8cff3e2efc68f648dc19b7d83326dd50931 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 28 May 2026 11:25:28 +0200 Subject: [PATCH 10/13] chore: Exclude without-toolbar-nested-iframe from a11y tests --- src/__a11y__/run-a11y-tests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__a11y__/run-a11y-tests.ts b/src/__a11y__/run-a11y-tests.ts index 616253c818..db0b3236e3 100644 --- a/src/__a11y__/run-a11y-tests.ts +++ b/src/__a11y__/run-a11y-tests.ts @@ -35,6 +35,7 @@ export default function runA11yTests(theme: Theme, mode: Mode, skip: string[] = 'undefined-texts', 'app-layout/with-error-boundaries', 'app-layout-toolbar/without-toolbar-nested', + 'app-layout-toolbar/without-toolbar-nested-iframe', ]; const testFunction = skipPages.includes(inputUrl) || From 6f7959fd2b5d0df11f6a6c55aa771863c231db2f Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 28 May 2026 12:21:45 +0200 Subject: [PATCH 11/13] chore: Formatting --- .../visual-refresh-toolbar/skeleton/multi-layout.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/multi-layout.ts b/src/app-layout/visual-refresh-toolbar/skeleton/multi-layout.ts index 3ced7c022d..376c99ded2 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/multi-layout.ts +++ b/src/app-layout/visual-refresh-toolbar/skeleton/multi-layout.ts @@ -29,9 +29,9 @@ export function useMultiAppLayout( setRegistration({ type: 'primary', discoveredProps: [] }); return; } - const unregister = awsuiPluginsInternal.appLayoutWidget.register(forceDeduplicationType, props => { - setRegistration(props as RegistrationState); - }); + const unregister = awsuiPluginsInternal.appLayoutWidget.register(forceDeduplicationType, props => + setRegistration(props as RegistrationState) + ); return () => { unregister(); setRegistration({ type: 'suspended' }); From a89e9be97be24b191f0531d48c3df4593e94a534 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 28 May 2026 14:03:34 +0200 Subject: [PATCH 12/13] chore: Remove redundant testing page --- .../without-toolbar-nested-iframe.page.tsx | 149 -------------- .../without-toolbar-nested.page.tsx | 181 ++++++++++-------- src/__a11y__/a11y-app-layout-toolbar.test.ts | 1 - src/__a11y__/run-a11y-tests.ts | 1 - 4 files changed, 103 insertions(+), 229 deletions(-) delete mode 100644 pages/app-layout-toolbar/without-toolbar-nested-iframe.page.tsx diff --git a/pages/app-layout-toolbar/without-toolbar-nested-iframe.page.tsx b/pages/app-layout-toolbar/without-toolbar-nested-iframe.page.tsx deleted file mode 100644 index 5b87e3e661..0000000000 --- a/pages/app-layout-toolbar/without-toolbar-nested-iframe.page.tsx +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React, { useRef, useState } from 'react'; - -import { AppLayoutToolbar, Button, ContentLayout, Header, HelpPanel, Link, SpaceBetween } from '~components'; -import { AppLayoutToolbarProps } from '~components/app-layout-toolbar'; -import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget'; - -import { Containers, CustomDrawerContent, Navigation } from '../app-layout/utils/content-blocks'; -import { drawerLabels } from '../app-layout/utils/drawers'; -import { leftDrawerPayload } from '../app-layout/utils/external-global-left-panel-widget'; -import appLayoutLabels from '../app-layout/utils/labels'; -import { IframeWrapper } from '../utils/iframe-wrapper'; -import ScreenshotArea from '../utils/screenshot-area'; - -registerLeftDrawer({ ...leftDrawerPayload, defaultActive: true }); - -export default function WithDrawers() { - const [activeDrawerId, setActiveDrawerId] = useState(null); - const [helpPathSlug, setHelpPathSlug] = useState('default'); - const [isToolsOpen, setIsToolsOpen] = useState(false); - const [isNavigationOpen, setIsNavigationOpen] = useState(true); - const pageLayoutRef = useRef(null); - - const drawersProps: Pick | null = { - activeDrawerId: activeDrawerId, - drawers: [ - { - ariaLabels: { - closeButton: 'ProHelp close button', - drawerName: 'ProHelp drawer content', - triggerButton: 'ProHelp trigger button', - resizeHandle: 'ProHelp resize handle', - }, - content: , - id: 'pro-help', - }, - ], - onDrawerChange: event => { - setActiveDrawerId(event.detail.activeDrawerId); - }, - }; - - return ( - - ( - -
{ - setHelpPathSlug('header'); - setIsToolsOpen(true); - pageLayoutRef.current?.focusToolsClose(); - }} - > - Info - - } - > - Page layout without the toolbar -
- - - - - - - - - - } - > -
{ - setHelpPathSlug('content'); - setIsToolsOpen(true); - }} - > - Info - - } - > - Content -
- - - } - /> - )} - /> - } - onToolsChange={event => { - setIsToolsOpen(event.detail.open); - }} - tools={} - toolsOpen={isToolsOpen} - navigationTriggerHide={true} - navigationOpen={isNavigationOpen} - navigation={} - onNavigationChange={event => setIsNavigationOpen(event.detail.open)} - {...drawersProps} - /> -
- ); -} - -function Info({ helpPathSlug }: { helpPathSlug: string }) { - return Info}>Here is some info for you: {helpPathSlug}; -} diff --git a/pages/app-layout-toolbar/without-toolbar-nested.page.tsx b/pages/app-layout-toolbar/without-toolbar-nested.page.tsx index c01a78b4ce..d656d30793 100644 --- a/pages/app-layout-toolbar/without-toolbar-nested.page.tsx +++ b/pages/app-layout-toolbar/without-toolbar-nested.page.tsx @@ -1,25 +1,34 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useRef, useState } from 'react'; +import React, { useContext, useRef, useState } from 'react'; -import { AppLayoutToolbar, Button, ContentLayout, Header, HelpPanel, Link, SpaceBetween } from '~components'; +import { AppLayoutToolbar, Button, ContentLayout, Header, HelpPanel, Link, SpaceBetween, Toggle } from '~components'; import { AppLayoutToolbarProps } from '~components/app-layout-toolbar'; import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget'; +import AppContext, { AppContextType } from '../app/app-context'; import { Containers, CustomDrawerContent, Navigation } from '../app-layout/utils/content-blocks'; import { drawerLabels } from '../app-layout/utils/drawers'; import { leftDrawerPayload } from '../app-layout/utils/external-global-left-panel-widget'; import appLayoutLabels from '../app-layout/utils/labels'; +import { IframeWrapper } from '../utils/iframe-wrapper'; import ScreenshotArea from '../utils/screenshot-area'; registerLeftDrawer({ ...leftDrawerPayload, defaultActive: true }); -export default function WithDrawers() { +type DemoContext = React.Context< + AppContextType<{ + hasNestedIframe: boolean | undefined; + }> +>; + +export default function WithoutToolbar() { const [activeDrawerId, setActiveDrawerId] = useState(null); const [helpPathSlug, setHelpPathSlug] = useState('default'); const [isToolsOpen, setIsToolsOpen] = useState(false); const [isNavigationOpen, setIsNavigationOpen] = useState(true); const pageLayoutRef = useRef(null); + const { urlParams, setUrlParams } = useContext(AppContext as DemoContext); const drawersProps: Pick | null = { activeDrawerId: activeDrawerId, @@ -40,6 +49,92 @@ export default function WithDrawers() { }, }; + const renderInternalAppLayout = () => ( + +
{ + setHelpPathSlug('header'); + setIsToolsOpen(true); + pageLayoutRef.current?.focusToolsClose(); + }} + > + Info + + } + > + Page layout without the toolbar +
+ + + { + setUrlParams({ hasNestedIframe: event.detail.checked }); + }} + > + Has nested iframe + + + + + + + + + } + > +
{ + setHelpPathSlug('content'); + setIsToolsOpen(true); + }} + > + Info + + } + > + Content +
+ + + } + /> + ); + return ( -
{ - setHelpPathSlug('header'); - setIsToolsOpen(true); - pageLayoutRef.current?.focusToolsClose(); - }} - > - Info - - } - > - Page layout without the toolbar -
- - - - - - - - - - } - > -
{ - setHelpPathSlug('content'); - setIsToolsOpen(true); - }} - > - Info - - } - > - Content -
- - - } - >
+ urlParams.hasNestedIframe ? ( + + ) : ( + renderInternalAppLayout() + ) } onToolsChange={event => { setIsToolsOpen(event.detail.open); diff --git a/src/__a11y__/a11y-app-layout-toolbar.test.ts b/src/__a11y__/a11y-app-layout-toolbar.test.ts index 9b486b771f..3658bd44ee 100644 --- a/src/__a11y__/a11y-app-layout-toolbar.test.ts +++ b/src/__a11y__/a11y-app-layout-toolbar.test.ts @@ -12,7 +12,6 @@ const EXCLUDED_PAGES = [ 'app-layout/multi-layout-global-drawer-child-layout', 'app-layout/with-error-boundaries', 'app-layout-toolbar/without-toolbar-nested', - 'app-layout-toolbar/without-toolbar-nested-iframe', ]; describe('A11y checks for app layout toolbar', () => { diff --git a/src/__a11y__/run-a11y-tests.ts b/src/__a11y__/run-a11y-tests.ts index db0b3236e3..616253c818 100644 --- a/src/__a11y__/run-a11y-tests.ts +++ b/src/__a11y__/run-a11y-tests.ts @@ -35,7 +35,6 @@ export default function runA11yTests(theme: Theme, mode: Mode, skip: string[] = 'undefined-texts', 'app-layout/with-error-boundaries', 'app-layout-toolbar/without-toolbar-nested', - 'app-layout-toolbar/without-toolbar-nested-iframe', ]; const testFunction = skipPages.includes(inputUrl) || From 6480b2be341c9794c8c62299dc60169aec4d02b0 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 28 May 2026 17:01:21 +0200 Subject: [PATCH 13/13] chore: comment --- src/__a11y__/run-a11y-tests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__a11y__/run-a11y-tests.ts b/src/__a11y__/run-a11y-tests.ts index 616253c818..0f83c77e4f 100644 --- a/src/__a11y__/run-a11y-tests.ts +++ b/src/__a11y__/run-a11y-tests.ts @@ -34,6 +34,7 @@ export default function runA11yTests(theme: Theme, mode: Mode, skip: string[] = // this page intentionally has issues to test the helper 'undefined-texts', 'app-layout/with-error-boundaries', + // nested app layouts aren't accessible, as every page should contain a level-one heading 'app-layout-toolbar/without-toolbar-nested', ]; const testFunction =