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..d656d30793 --- /dev/null +++ b/pages/app-layout-toolbar/without-toolbar-nested.page.tsx @@ -0,0 +1,168 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useContext, useRef, useState } from 'react'; + +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 }); + +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, + 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); + }, + }; + + 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 ( + + + ) : ( + renderInternalAppLayout() + ) + } + 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..648b2b2b9f 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, defaultActive: true }); + export default function WithDrawers() { const [activeDrawerId, setActiveDrawerId] = useState(null); const [helpPathSlug, setHelpPathSlug] = useState('default'); @@ -39,6 +43,7 @@ export default function WithDrawers() { return ( + } 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/__a11y__/run-a11y-tests.ts b/src/__a11y__/run-a11y-tests.ts index a7e6411493..0f83c77e4f 100644 --- a/src/__a11y__/run-a11y-tests.ts +++ b/src/__a11y__/run-a11y-tests.ts @@ -34,6 +34,8 @@ 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 = skipPages.includes(inputUrl) || diff --git a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx index 7c5d0e5488..a67edb7d1a 100644 --- a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx @@ -302,6 +302,65 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, ({ size }) => { } }); + describe('left drawer without toolbar', () => { + 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 (__forceEnableRuntimeMessages is provided)', () => { + 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 (__forceEnableRuntimeMessages is provided)', () => { + 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(() => { diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss index c0713f92bb..92879161e7 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss @@ -214,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/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..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 @@ -79,6 +79,7 @@ export const useAppLayout = ( const onMountRootRef = useCallback(node => { setIsNested(getIsNestedInAppLayout(node)); }, []); + const { __forceEnableRuntimeMessages: forceEnableRuntimeMessages } = rest as any; const [toolsOpen = false, setToolsOpen] = useControllable(controlledToolsOpen, onToolsChange, false, { componentName: 'AppLayout', @@ -239,7 +240,7 @@ export const useAppLayout = ( } }; - useWidgetMessages(hasToolbar, message => { + useWidgetMessages(hasToolbar || forceEnableRuntimeMessages, message => { if (message.type === 'expandDrawer' || message.type === 'exitExpandedMode') { drawerGenericMessageHandler(message); return; 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 && (