-
Notifications
You must be signed in to change notification settings - Fork 231
feat: Left panel without toolbar in AppLayout component #4553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
26bb4d8
abd7aab
3ab8cd7
2a22661
e365c6b
a277ea8
3e8529f
9329623
65329a7
8784e8c
6f7959f
a89e9be
6480b2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string | null>(null); | ||
| const [helpPathSlug, setHelpPathSlug] = useState<string>('default'); | ||
| const [isToolsOpen, setIsToolsOpen] = useState(false); | ||
| const [isNavigationOpen, setIsNavigationOpen] = useState(true); | ||
| const pageLayoutRef = useRef<AppLayoutToolbarProps.Ref>(null); | ||
| const { urlParams, setUrlParams } = useContext(AppContext as DemoContext); | ||
|
|
||
| const drawersProps: Pick<AppLayoutToolbarProps, 'activeDrawerId' | 'onDrawerChange' | 'drawers'> | null = { | ||
| activeDrawerId: activeDrawerId, | ||
| drawers: [ | ||
| { | ||
| ariaLabels: { | ||
| closeButton: 'ProHelp close button', | ||
| drawerName: 'ProHelp drawer content', | ||
| triggerButton: 'ProHelp trigger button', | ||
| resizeHandle: 'ProHelp resize handle', | ||
| }, | ||
| content: <CustomDrawerContent />, | ||
| id: 'pro-help', | ||
| }, | ||
| ], | ||
| onDrawerChange: event => { | ||
| setActiveDrawerId(event.detail.activeDrawerId); | ||
| }, | ||
| }; | ||
|
|
||
| const renderInternalAppLayout = () => ( | ||
| <AppLayoutToolbar | ||
| toolsHide={true} | ||
| navigationHide={true} | ||
| navigationTriggerHide={true} | ||
| content={ | ||
| <ContentLayout | ||
| disableOverlap={true} | ||
| header={ | ||
| <SpaceBetween size="m"> | ||
| <Header | ||
| variant="h1" | ||
| description="Sometimes you need custom triggers for drawers and navigation to get the job done." | ||
| info={ | ||
| <Link | ||
| data-testid="info-link-header" | ||
| variant="info" | ||
| onFollow={() => { | ||
| setHelpPathSlug('header'); | ||
| setIsToolsOpen(true); | ||
| pageLayoutRef.current?.focusToolsClose(); | ||
| }} | ||
| > | ||
| Info | ||
| </Link> | ||
| } | ||
| > | ||
| Page layout without the toolbar | ||
| </Header> | ||
|
|
||
| <SpaceBetween size="xs"> | ||
| <Toggle | ||
| checked={urlParams.hasNestedIframe ?? false} | ||
| onChange={event => { | ||
| setUrlParams({ hasNestedIframe: event.detail.checked }); | ||
| }} | ||
| > | ||
| Has nested iframe | ||
| </Toggle> | ||
| <Button | ||
| onClick={() => { | ||
| setIsNavigationOpen(current => !current); | ||
| pageLayoutRef.current?.focusNavigation(); | ||
| }} | ||
| > | ||
| Toggle navigation | ||
| </Button> | ||
|
|
||
| <Button | ||
| onClick={() => { | ||
| setActiveDrawerId('pro-help'); | ||
| pageLayoutRef.current?.focusActiveDrawer(); | ||
| }} | ||
| > | ||
| Open a drawer without trigger | ||
| </Button> | ||
| <Button onClick={() => setActiveDrawerId(null)}>Close a drawer without trigger</Button> | ||
| <Button onClick={() => updateDrawer({ type: 'openDrawer', payload: { id: 'ai-panel' } })}> | ||
| Open the left panel | ||
| </Button> | ||
| </SpaceBetween> | ||
| </SpaceBetween> | ||
| } | ||
| > | ||
| <Header | ||
| info={ | ||
| <Link | ||
| data-testid="info-link-content" | ||
| variant="info" | ||
| onFollow={() => { | ||
| setHelpPathSlug('content'); | ||
| setIsToolsOpen(true); | ||
| }} | ||
| > | ||
| Info | ||
| </Link> | ||
| } | ||
| > | ||
| Content | ||
| </Header> | ||
| <Containers /> | ||
| </ContentLayout> | ||
| } | ||
| /> | ||
| ); | ||
|
|
||
| return ( | ||
| <ScreenshotArea gutters={false}> | ||
| <AppLayoutToolbar | ||
| {...{ __forceEnableRuntimeMessages: true }} | ||
| ariaLabels={{ ...appLayoutLabels, ...drawerLabels }} | ||
| ref={pageLayoutRef} | ||
| content={ | ||
| urlParams.hasNestedIframe ? ( | ||
| <IframeWrapper id="smth" AppComponent={renderInternalAppLayout} /> | ||
| ) : ( | ||
| renderInternalAppLayout() | ||
| ) | ||
| } | ||
| onToolsChange={event => { | ||
| setIsToolsOpen(event.detail.open); | ||
| }} | ||
| tools={<Info helpPathSlug={helpPathSlug} />} | ||
| toolsOpen={isToolsOpen} | ||
| navigationTriggerHide={true} | ||
| navigationOpen={isNavigationOpen} | ||
| navigation={<Navigation />} | ||
| onNavigationChange={event => setIsNavigationOpen(event.detail.open)} | ||
| {...drawersProps} | ||
| /> | ||
| </ScreenshotArea> | ||
| ); | ||
| } | ||
|
|
||
| function Info({ helpPathSlug }: { helpPathSlug: string }) { | ||
| return <HelpPanel header={<h2>Info</h2>}>Here is some info for you: {helpPathSlug}</HelpPanel>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the reason for excluding these?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because nested app layouts aren't accessible and there is no good way to fix them
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we add a comment explaining the accessibility issue?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
| ]; | ||
| const testFunction = | ||
| skipPages.includes(inputUrl) || | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These styles are for showing a round-corner for content area. Since the pseudo toolbar should not occupy any space, there is no way to apply styles to it directly |
||
| &: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; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,7 +42,7 @@ export const mergeProps: MergeProps = (ownProps, additionalProps) => { | |
| toolbar.onActiveGlobalDrawersChange = props.onActiveGlobalDrawersChange; | ||
| } | ||
| if ( | ||
| props.aiDrawer && | ||
| props.aiDrawer?.trigger && | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these are toolbar only props, so if the drawer is trigger-less, its props shouldn't be forwarded to the toolbar |
||
| props.aiDrawerFocusRef && | ||
| !checkAlreadyExists(!!toolbar.aiDrawerFocusRef, 'aiDrawerFocusRef') | ||
| ) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 => { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Introduced an opt-in flag here. I considered detecting whether the AppLayout is the outermost instance in nested layouts, but that doesn't work with isolated iframes and would introduce a regression |
||
| if (message.type === 'expandDrawer' || message.type === 'exitExpandedMode') { | ||
| drawerGenericMessageHandler(message); | ||
| return; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will introduce a screenshot test on this page