diff --git a/pages/app-layout/with-drawers-empty.page.tsx b/pages/app-layout/with-drawers-empty.page.tsx
new file mode 100644
index 0000000000..fe12d0bf8e
--- /dev/null
+++ b/pages/app-layout/with-drawers-empty.page.tsx
@@ -0,0 +1,58 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React from 'react';
+import { AppLayout, ContentLayout, Header, SplitPanel } from '~components';
+import appLayoutLabels from './utils/labels';
+import { Breadcrumbs, Containers } from './utils/content-blocks';
+import ScreenshotArea from '../utils/screenshot-area';
+
+export default function WithDrawers() {
+ const drawers = {
+ drawers: {
+ items: [],
+ },
+ };
+
+ return (
+
+ }
+ content={
+
+ Page without drawers
+
+ }
+ >
+
+
+ }
+ splitPanel={
+
+ This is the Split Panel!
+
+ }
+ {...drawers}
+ />
+
+ );
+}
diff --git a/src/app-layout/__tests__/drawers.test.tsx b/src/app-layout/__tests__/drawers.test.tsx
index 6fc63f7798..a20c491d17 100644
--- a/src/app-layout/__tests__/drawers.test.tsx
+++ b/src/app-layout/__tests__/drawers.test.tsx
@@ -1,12 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
-import { describeEachAppLayout, renderComponent, singleDrawer, manyDrawers, singleDrawerOpen } from './utils';
+import { describeEachAppLayout, renderComponent, singleDrawer, manyDrawers } from './utils';
import createWrapper from '../../../lib/components/test-utils/dom';
import { render } from '@testing-library/react';
import AppLayout from '../../../lib/components/app-layout';
-import { TOOLS_DRAWER_ID } from '../../../lib/components/app-layout/utils/use-drawers';
jest.mock('../../../lib/components/internal/hooks/use-mobile', () => ({
useMobile: jest.fn().mockReturnValue(true),
@@ -17,7 +16,7 @@ jest.mock('@cloudscape-design/component-toolkit', () => ({
useContainerQuery: () => [100, () => {}],
}));
-describeEachAppLayout(size => {
+describeEachAppLayout(() => {
test(`should not render drawer when it is not defined`, () => {
const { wrapper, rerender } = renderComponent();
expect(wrapper.findDrawersTriggers()).toHaveLength(1);
@@ -35,82 +34,14 @@ describeEachAppLayout(size => {
const { wrapper } = renderComponent();
expect(wrapper.findDrawersTriggers()).toHaveLength(0);
- expect(wrapper.findToolsToggle()).toBeTruthy();
+ expect(wrapper.findToolsToggle()).toBeFalsy();
});
- test('should apply drawers treatment to the tools if at least one other drawer is provided', () => {
- const { wrapper } = renderComponent();
- expect(wrapper.findDrawersTriggers()).toHaveLength(2);
- expect(wrapper.findToolsToggle()).toBeTruthy();
- });
-
- test('renders drawers with the tools', () => {
+ test('ignores tools when drawers API is used', () => {
const { wrapper } = renderComponent();
- expect(wrapper.findDrawersTriggers()).toHaveLength(2);
- });
-
- // this behavior is no longer supported for compatibility with runtime API
- test.skip('should respect toolsOpen property when merging into drawers', () => {
- const { wrapper } = renderComponent();
-
- expect(wrapper.findDrawerTriggerById(TOOLS_DRAWER_ID)!.getElement()).toHaveAttribute('aria-expanded', 'true');
- expect(wrapper.findDrawerTriggerById('security')!.getElement()).toHaveAttribute('aria-expanded', 'false');
- expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('Tools content');
- });
-
- test('should fire tools change event when closing tools panel while drawers are present', () => {
- const onToolsChange = jest.fn();
- const { wrapper } = renderComponent(
- onToolsChange(event.detail)} {...singleDrawer} />
- );
-
- wrapper.findToolsToggle().click();
- expect(onToolsChange).toHaveBeenCalledWith({ open: true });
-
- onToolsChange.mockClear();
- wrapper.findToolsClose().click();
- expect(onToolsChange).toHaveBeenCalledWith({ open: false });
- });
-
- // drawers render full screen on mobile sizes, switching open drawers does not work there
- if (size === 'desktop') {
- test('should fire tools close event when switching from tools to another drawer', () => {
- const onToolsChange = jest.fn();
- const { wrapper } = renderComponent(
- onToolsChange(event.detail)}
- {...singleDrawer}
- />
- );
-
- wrapper.findDrawerTriggerById('security')!.click();
- expect(onToolsChange).toHaveBeenCalledWith({ open: false });
- });
-
- test('should fire tools open event when switching from another drawer to tools', () => {
- const onToolsChange = jest.fn();
- const { wrapper } = renderComponent(
- onToolsChange(event.detail)}
- {...singleDrawerOpen}
- />
- );
- wrapper.findToolsToggle().click();
- expect(onToolsChange).toHaveBeenCalledWith({ open: true });
- });
- }
-
- test('activeDrawerId has priority over toolsOpen', () => {
- const { wrapper } = renderComponent();
-
- expect(wrapper.findDrawerTriggerById(TOOLS_DRAWER_ID)!.getElement()).toHaveAttribute('aria-expanded', 'false');
- expect(wrapper.findDrawerTriggerById('security')!.getElement()).toHaveAttribute('aria-expanded', 'true');
- expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('Security');
+ expect(wrapper.findToolsToggle()).toBeFalsy();
+ expect(wrapper.findDrawersTriggers()).toHaveLength(1);
});
test('should open active drawer on click of overflow item', () => {
diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx
index 8fa8eccc31..9d3059cc68 100644
--- a/src/app-layout/__tests__/runtime-drawers.test.tsx
+++ b/src/app-layout/__tests__/runtime-drawers.test.tsx
@@ -180,6 +180,30 @@ describeEachAppLayout(size => {
expect(onToolsChange).toHaveBeenCalledWith({ open: false });
expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('runtime drawer content');
+ onToolsChange.mockReset();
+ wrapper.findToolsToggle().click();
+ expect(onToolsChange).toHaveBeenCalledWith({ open: true });
+ });
+
+ test('should fire tools close event when switching from tools to another drawer', async () => {
+ awsuiPlugins.appLayout.registerDrawer(drawerDefaults);
+ const onToolsChange = jest.fn();
+ const { wrapper } = await renderComponent(
+ onToolsChange(event.detail)} />
+ );
+
+ wrapper.findDrawerTriggerById(drawerDefaults.id)!.click();
+ expect(onToolsChange).toHaveBeenCalledWith({ open: false });
+ });
+
+ test('should fire tools open event when switching from another drawer to tools', async () => {
+ awsuiPlugins.appLayout.registerDrawer({ ...drawerDefaults, defaultActive: true });
+ const onToolsChange = jest.fn();
+ const { wrapper } = await renderComponent(
+ onToolsChange(event.detail)} />
+ );
+ expect(wrapper.findActiveDrawer()!.getElement()).toBeInTheDocument();
+
wrapper.findToolsToggle().click();
expect(onToolsChange).toHaveBeenCalledWith({ open: true });
});
@@ -356,7 +380,7 @@ describeEachAppLayout(size => {
]);
});
- test('allows mixing static and runtime drawers', async () => {
+ test('ignores tools when drawers are present', async () => {
awsuiPlugins.appLayout.registerDrawer({ ...drawerDefaults, id: 'aaa', ariaLabels: { triggerButton: 'aaa' } });
awsuiPlugins.appLayout.registerDrawer({
...drawerDefaults,
@@ -379,7 +403,6 @@ describeEachAppLayout(size => {
);
expect(wrapper.findDrawersTriggers().map(trigger => trigger.getElement().getAttribute('aria-label'))).toEqual([
- 'tools toggle',
'bbb',
'ddd',
'aaa',
@@ -387,4 +410,19 @@ describeEachAppLayout(size => {
]);
});
});
+
+ test('should fire tools change event when closing tools panel while drawers are present', async () => {
+ const onToolsChange = jest.fn();
+ awsuiPlugins.appLayout.registerDrawer(drawerDefaults);
+ const { wrapper } = await renderComponent(
+ onToolsChange(event.detail)} />
+ );
+
+ wrapper.findToolsToggle().click();
+ expect(onToolsChange).toHaveBeenCalledWith({ open: true });
+
+ onToolsChange.mockClear();
+ wrapper.findToolsClose().click();
+ expect(onToolsChange).toHaveBeenCalledWith({ open: false });
+ });
});
diff --git a/src/app-layout/drawer/index.tsx b/src/app-layout/drawer/index.tsx
index e715e50e3f..36448f0470 100644
--- a/src/app-layout/drawer/index.tsx
+++ b/src/app-layout/drawer/index.tsx
@@ -110,7 +110,7 @@ export const Drawer = React.forwardRef(
style={{ width: drawerContentWidth, top: topOffset, bottom: bottomOffset }}
className={clsx(styles['drawer-content'], styles['drawer-content-clickable'], contentClassName)}
>
- {!isMobile && regularOpenButton}
+ {!isMobile && !drawers && regularOpenButton}
{resizeHandle}
0;
+ } = useDrawers(props as InternalDrawerProps, {
+ ariaLabels,
+ tools,
+ toolsOpen,
+ toolsHide,
+ toolsWidth,
+ onToolsChange,
+ });
+ const hasDrawers = !!drawers;
const { refs: navigationRefs, setFocus: focusNavButtons } = useFocusControl(navigationOpen);
const {
@@ -248,7 +255,11 @@ const OldAppLayout = React.forwardRef(
const effectiveNavigationWidth = navigationHide ? 0 : navigationOpen ? navigationWidth : closedDrawerWidth;
const getEffectiveToolsWidth = () => {
- if (toolsHide && (!splitPanelDisplayed || splitPanelPreferences?.position !== 'side') && drawers.length === 0) {
+ if (
+ toolsHide &&
+ (!splitPanelDisplayed || splitPanelPreferences?.position !== 'side') &&
+ (!drawers || drawers.length === 0)
+ ) {
return 0;
}
@@ -507,7 +518,6 @@ const OldAppLayout = React.forwardRef(
onChange: changeDetail => {
onActiveDrawerChange(changeDetail.activeDrawerId);
if (changeDetail.activeDrawerId !== activeDrawerId) {
- onToolsToggle(changeDetail.activeDrawerId === TOOLS_DRAWER_ID);
focusDrawersButtons();
setDrawerLastInteraction({ type: 'open' });
}
@@ -638,66 +648,67 @@ const OldAppLayout = React.forwardRef(
)}
- {((hasDrawers && activeDrawerId) || (!hasDrawers && !toolsHide)) &&
- (hasDrawers ? (
- {
- onToolsToggle(false);
- setDrawerLastInteraction({ type: 'close' });
- onActiveDrawerChange(changeDetail.activeDrawerId);
- },
- }}
- size={!isResizeInvalid ? activeDrawerSize : toolsWidth}
- onResize={changeDetail => onActiveDrawerResize(changeDetail)}
- refs={drawerRefs}
- getMaxWidth={getDrawerMaxWidth}
- >
- {activeDrawer?.content}
-
- ) : (
-
- {tools}
-
- ))}
+ {hasDrawers
+ ? activeDrawerId && (
+ {
+ /*noop in this mode*/
+ }}
+ isOpen={true}
+ toggleRefs={toolsRefs}
+ type="tools"
+ onLoseFocus={loseDrawersFocus}
+ activeDrawer={activeDrawer}
+ drawers={{
+ items: drawers,
+ activeDrawerId: activeDrawerId,
+ onChange: changeDetail => {
+ focusToolsButtons();
+ setDrawerLastInteraction({ type: 'close' });
+ onActiveDrawerChange(changeDetail.activeDrawerId);
+ },
+ }}
+ size={!isResizeInvalid ? activeDrawerSize : toolsWidth}
+ onResize={changeDetail => onActiveDrawerResize(changeDetail)}
+ refs={drawerRefs}
+ getMaxWidth={getDrawerMaxWidth}
+ >
+ {activeDrawer?.content}
+
+ )
+ : !toolsHide && (
+
+ {tools}
+
+ )}
{hasDrawers && (
{
if (activeDrawerId !== changeDetail.activeDrawerId) {
- onToolsToggle(changeDetail.activeDrawerId === TOOLS_DRAWER_ID);
+ focusToolsButtons();
focusDrawersButtons();
setDrawerLastInteraction({ type: 'open' });
}
diff --git a/src/app-layout/mobile-toolbar/index.tsx b/src/app-layout/mobile-toolbar/index.tsx
index 1b7574d49e..9bf5151cb2 100644
--- a/src/app-layout/mobile-toolbar/index.tsx
+++ b/src/app-layout/mobile-toolbar/index.tsx
@@ -137,11 +137,10 @@ export function MobileToolbar({
onClick={() => drawers.onChange({ activeDrawerId: item.id })}
>
void;
+
+function getToolsDrawerItem(props: ToolsProps): DrawerItem | null {
if (props.toolsHide) {
return null;
}
@@ -45,9 +48,10 @@ function getToolsDrawerItem(props: ToolsProps) {
function useRuntimeDrawers(
disableRuntimeDrawers: boolean | undefined,
activeDrawerId: string | undefined,
- onActiveDrawerChange: (id: string | undefined) => void
+ onActiveDrawerChange: DrawerChangeHandler
) {
const [runtimeDrawers, setRuntimeDrawers] = useState({ before: [], after: [] });
+ const onActiveDrawerChangeStable = useStableCallback(onActiveDrawerChange);
const drawerWasOpenRef = useRef(false);
drawerWasOpenRef.current = drawerWasOpenRef.current || !!activeDrawerId;
@@ -61,7 +65,7 @@ function useRuntimeDrawers(
if (!drawerWasOpenRef.current) {
const defaultActiveDrawer = sortByPriority(drawers).find(drawer => drawer.defaultActive);
if (defaultActiveDrawer) {
- onActiveDrawerChange(defaultActiveDrawer.id);
+ onActiveDrawerChangeStable(defaultActiveDrawer.id);
}
}
});
@@ -69,11 +73,24 @@ function useRuntimeDrawers(
unsubscribe();
setRuntimeDrawers({ before: [], after: [] });
};
- }, [disableRuntimeDrawers, onActiveDrawerChange]);
+ }, [disableRuntimeDrawers, onActiveDrawerChangeStable]);
return runtimeDrawers;
}
+function applyToolsDrawer(toolsProps: ToolsProps, runtimeDrawers: DrawersLayout) {
+ const drawers = [...runtimeDrawers.before, ...runtimeDrawers.after];
+ if (drawers.length === 0) {
+ return null;
+ }
+ const toolsItem = getToolsDrawerItem(toolsProps);
+ if (toolsItem) {
+ drawers.unshift(toolsItem);
+ }
+
+ return drawers;
+}
+
export function useDrawers(
{
drawers: ownDrawers,
@@ -81,8 +98,6 @@ export function useDrawers(
}: InternalDrawerProps & { __disableRuntimeDrawers?: boolean },
toolsProps: ToolsProps
) {
- const toolsDrawer = getToolsDrawerItem(toolsProps);
-
const [activeDrawerId, setActiveDrawerId] = useControllable(
ownDrawers?.activeDrawerId,
ownDrawers?.onChange,
@@ -95,28 +110,32 @@ export function useDrawers(
);
const [drawerSizes, setDrawerSizes] = useState>({});
- const onActiveDrawerChange = useStableCallback((newDrawerId: string | undefined) => {
+ function onActiveDrawerResize({ id, size }: { id: string; size: number }) {
+ setDrawerSizes(oldSizes => ({ ...oldSizes, [id]: size }));
+ fireNonCancelableEvent(ownDrawers?.onResize, { id, size });
+ }
+
+ function onActiveDrawerChange(newDrawerId: string | undefined) {
setActiveDrawerId(newDrawerId);
- fireNonCancelableEvent(ownDrawers?.onChange, newDrawerId);
- });
+ if (hasOwnDrawers) {
+ fireNonCancelableEvent(ownDrawers?.onChange, newDrawerId);
+ }
+ if (!toolsProps.toolsHide) {
+ fireNonCancelableEvent(toolsProps.onToolsChange, { open: newDrawerId === TOOLS_DRAWER_ID });
+ }
+ }
+ const hasOwnDrawers = !!ownDrawers?.items;
const runtimeDrawers = useRuntimeDrawers(disableRuntimeDrawers, activeDrawerId, onActiveDrawerChange);
- const combinedDrawers = [...runtimeDrawers.before, ...(ownDrawers?.items ?? []), ...runtimeDrawers.after];
- if (toolsDrawer && combinedDrawers.length > 0) {
- combinedDrawers.unshift(toolsDrawer);
- }
+ const combinedDrawers = hasOwnDrawers
+ ? [...runtimeDrawers.before, ...ownDrawers.items, ...runtimeDrawers.after]
+ : applyToolsDrawer(toolsProps, runtimeDrawers);
// support toolsOpen in runtime-drawers-only mode
- let activeDrawerIdResolved =
- toolsProps.toolsOpen && (ownDrawers?.items ?? []).length === 0 ? TOOLS_DRAWER_ID : activeDrawerId;
- const activeDrawer = combinedDrawers.find(drawer => drawer.id === activeDrawerIdResolved);
+ let activeDrawerIdResolved = toolsProps.toolsOpen && !hasOwnDrawers ? TOOLS_DRAWER_ID : activeDrawerId;
+ const activeDrawer = combinedDrawers?.find(drawer => drawer.id === activeDrawerIdResolved);
// ensure that id is only defined when the drawer exists
activeDrawerIdResolved = activeDrawer?.id;
- function onActiveDrawerResize({ id, size }: { id: string; size: number }) {
- setDrawerSizes(oldSizes => ({ ...oldSizes, [id]: size }));
- fireNonCancelableEvent(ownDrawers?.onResize, { id, size });
- }
-
return {
ariaLabel: ownDrawers?.ariaLabel,
overflowAriaLabel: ownDrawers?.overflowAriaLabel,
diff --git a/src/app-layout/visual-refresh/context.tsx b/src/app-layout/visual-refresh/context.tsx
index ac4efb65e9..1a3299cae6 100644
--- a/src/app-layout/visual-refresh/context.tsx
+++ b/src/app-layout/visual-refresh/context.tsx
@@ -36,7 +36,7 @@ import { useUniqueId } from '../../internal/hooks/use-unique-id';
interface AppLayoutInternals extends AppLayoutProps {
activeDrawerId: string | undefined;
- drawers: Array;
+ drawers: Array | null;
drawersAriaLabel: string | undefined;
drawersOverflowAriaLabel: string | undefined;
drawersRefs: DrawerFocusControlRefs;
@@ -398,6 +398,7 @@ export const AppLayoutInternalsProvider = React.forwardRef(
toolsOpen: isToolsOpen,
tools: props.tools,
toolsWidth,
+ onToolsChange: props.onToolsChange,
});
const [drawersMaxWidth, setDrawersMaxWidth] = useState(toolsWidth);
@@ -428,8 +429,10 @@ export const AppLayoutInternalsProvider = React.forwardRef(
setDrawerLastInteraction({ type: activeDrawerId ? 'close' : 'open' });
};
- const drawersTriggerCount =
- drawers.length + (splitPanelDisplayed && splitPanelPosition === 'side' ? 1 : 0) + (!toolsHide ? 1 : 0);
+ let drawersTriggerCount = drawers ? drawers.length : !toolsHide ? 1 : 0;
+ if (splitPanelDisplayed && splitPanelPosition === 'side') {
+ drawersTriggerCount++;
+ }
const hasOpenDrawer =
activeDrawerId !== undefined ||
(!toolsHide && isToolsOpen) ||
diff --git a/src/app-layout/visual-refresh/drawers.tsx b/src/app-layout/visual-refresh/drawers.tsx
index 1a8cc4452c..2aacdc39fd 100644
--- a/src/app-layout/visual-refresh/drawers.tsx
+++ b/src/app-layout/visual-refresh/drawers.tsx
@@ -26,6 +26,7 @@ export default function Drawers() {
const {
disableBodyScroll,
drawers,
+ drawersTriggerCount,
hasDrawerViewportOverlay,
hasOpenDrawer,
isNavigationOpen,
@@ -35,7 +36,7 @@ export default function Drawers() {
const isUnfocusable = hasDrawerViewportOverlay && isNavigationOpen && !navigationHide;
- if (drawers.length === 0) {
+ if (!drawers || drawersTriggerCount === 0) {
return null;
}
@@ -66,7 +67,6 @@ function ActiveDrawer() {
isMobile,
isNavigationOpen,
navigationHide,
- toolsRefs,
loseDrawersFocus,
resizeHandle,
drawerSize,
@@ -74,7 +74,7 @@ function ActiveDrawer() {
drawerRef,
} = useAppLayoutInternals();
- const activeDrawer = drawers.find(item => item.id === activeDrawerId) ?? null;
+ const activeDrawer = drawers?.find(item => item.id === activeDrawerId) ?? null;
const computedAriaLabels = {
closeButton: activeDrawerId ? activeDrawer?.ariaLabels?.closeButton : ariaLabels?.toolsClose,
@@ -122,7 +122,7 @@ function ActiveDrawer() {
handleDrawersClick(activeDrawerId ?? undefined);
handleToolsClick(false);
}}
- ref={isToolsDrawer ? toolsRefs.close : drawersRefs.close}
+ ref={drawersRefs.close}
variant="icon"
/>
@@ -148,10 +148,8 @@ function DesktopTriggers() {
drawersTriggerCount,
handleDrawersClick,
handleSplitPanelClick,
- handleToolsClick,
hasOpenDrawer,
isSplitPanelOpen,
- isToolsOpen,
splitPanel,
splitPanelControlId,
splitPanelDisplayed,
@@ -163,7 +161,7 @@ function DesktopTriggers() {
} = useAppLayoutInternals();
const hasMultipleTriggers = drawersTriggerCount > 1;
- const hasSplitPanel = splitPanel && splitPanelDisplayed && splitPanelPosition === 'side' ? true : false;
+ const hasSplitPanel = splitPanel && splitPanelDisplayed && splitPanelPosition === 'side';
const previousActiveDrawerId = useRef(activeDrawerId);
const [containerHeight, triggersContainerRef] = useContainerQuery(rect => rect.contentBoxHeight);
@@ -195,18 +193,9 @@ function DesktopTriggers() {
return 0;
};
- const { visibleItems, overflowItems } = splitItems(drawers, getIndexOfOverflowItem(), activeDrawerId);
+ const { visibleItems, overflowItems } = splitItems(drawers ?? undefined, getIndexOfOverflowItem(), activeDrawerId);
const overflowMenuHasBadge = !!overflowItems.find(item => item.badge);
- function handleItemClick(itemId: string | undefined) {
- if (itemId === TOOLS_DRAWER_ID) {
- handleToolsClick(!isToolsOpen, true);
- } else {
- handleToolsClick(false, true);
- }
- handleDrawersClick(itemId);
- }
-
return (