Skip to content

Commit 238e746

Browse files
committed
Add onLayoutChange event
1 parent 493dd87 commit 238e746

File tree

6 files changed

+255
-65
lines changed

6 files changed

+255
-65
lines changed

pages/app-layout/utils/external-global-left-panel-widget.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// SPDX-License-Identifier: Apache-2.0
33
import React, { useState } from 'react';
44

5-
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
6-
75
import { Box, Button, PanelLayout } from '~components';
86
import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget';
97
import { mount, unmount } from '~mount';
@@ -19,8 +17,7 @@ const AIDrawer = () => {
1917
const [hasArtifact, setHasArtifact] = useState(false);
2018
const [artifactLoaded, setArtifactLoaded] = useState(false);
2119
const [chatSize, setChatSize] = useState(CHAT_SIZE);
22-
const [_maxPanelSize, ref] = useContainerQuery(entry => entry.contentBoxWidth - MIN_ARTIFACT_SIZE);
23-
const maxPanelSize = _maxPanelSize ?? Number.MAX_SAFE_INTEGER;
20+
const [maxPanelSize, setMaxPanelSize] = useState(Number.MAX_SAFE_INTEGER);
2421
const constrainedChatSize = Math.min(chatSize, maxPanelSize);
2522
const collapsed = constrainedChatSize < MIN_CHAT_SIZE;
2623

@@ -87,18 +84,17 @@ const AIDrawer = () => {
8784
</Box>
8885
);
8986
return (
90-
<div ref={ref} style={{ width: '100%', overflow: 'hidden' }}>
91-
<PanelLayout
92-
resizable={true}
93-
panelSize={constrainedChatSize}
94-
maxPanelSize={maxPanelSize}
95-
minPanelSize={MIN_CHAT_SIZE}
96-
onPanelResize={({ detail }) => setChatSize(detail.panelSize)}
97-
panelContent={chatContent}
98-
mainContent={<div className={styles['ai-artifact-panel']}>{artifactContent}</div>}
99-
display={hasArtifact ? (collapsed ? 'main-only' : 'all') : 'panel-only'}
100-
/>
101-
</div>
87+
<PanelLayout
88+
resizable={true}
89+
panelSize={constrainedChatSize}
90+
maxPanelSize={maxPanelSize}
91+
minPanelSize={MIN_CHAT_SIZE}
92+
onPanelResize={({ detail }) => setChatSize(detail.panelSize)}
93+
onLayoutChange={({ detail }) => setMaxPanelSize(detail.totalSize - MIN_ARTIFACT_SIZE)}
94+
panelContent={chatContent}
95+
mainContent={<div className={styles['ai-artifact-panel']}>{artifactContent}</div>}
96+
display={hasArtifact ? (collapsed ? 'main-only' : 'all') : 'panel-only'}
97+
/>
10298
);
10399
};
104100

pages/panel-layout/app-layout-panel.page.tsx

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// SPDX-License-Identifier: Apache-2.0
33
import React, { useContext, useState } from 'react';
44

5-
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
6-
75
import { Box, Checkbox, Drawer, FormField, Header, Input, SegmentedControl, SpaceBetween } from '~components';
86
import AppLayout from '~components/app-layout';
97
import Button from '~components/button';
@@ -38,56 +36,46 @@ const PanelLayoutContent = ({
3836
panelPosition,
3937
}: PanelLayoutContentProps) => {
4038
const [size, setSize] = useState(Math.max(200, minPanelSize));
39+
const [actualMaxPanelSize, setActualMaxPanelSize] = useState(size);
4140

42-
const [_actualMaxPanelSize, ref] = useContainerQuery(
43-
entry => Math.min(entry.contentBoxWidth - minContentSize, maxPanelSize),
44-
[minContentSize, maxPanelSize]
45-
);
46-
const actualMaxPanelSize = _actualMaxPanelSize ?? maxPanelSize;
4741
const actualSize = Math.min(size, actualMaxPanelSize);
48-
4942
const collapsed = actualMaxPanelSize < minPanelSize;
5043

5144
return (
52-
<div ref={ref} style={{ height: '100%', overflow: 'hidden' }}>
53-
{collapsed ? (
54-
'Collapsed view'
55-
) : (
56-
<PanelLayout
57-
panelSize={actualSize}
58-
minPanelSize={minPanelSize}
59-
maxPanelSize={actualMaxPanelSize}
60-
resizable={true}
61-
onPanelResize={({ detail }) => setSize(detail.panelSize)}
62-
display={display}
63-
panelPosition={panelPosition}
64-
mainFocusable={longMainContent && !buttons ? { ariaLabel: 'Main content' } : undefined}
65-
panelFocusable={longPanelContent && !buttons ? { ariaLabel: 'Panel content' } : undefined}
66-
panelContent={
67-
<Box padding="m">
68-
<Header>Panel content</Header>
69-
{new Array(longPanelContent ? 20 : 1)
70-
.fill('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')
71-
.map((t, i) => (
72-
<div key={i}>{t}</div>
73-
))}
74-
{buttons && <Button>Button</Button>}
75-
</Box>
76-
}
77-
mainContent={
78-
<Box padding="m">
79-
<Header>Main content</Header>
80-
{new Array(longMainContent ? 200 : 1)
81-
.fill('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')
82-
.map((t, i) => (
83-
<div key={i}>{t}</div>
84-
))}
85-
{buttons && <Button>button</Button>}
86-
</Box>
87-
}
88-
/>
89-
)}
90-
</div>
45+
<PanelLayout
46+
panelSize={actualSize}
47+
minPanelSize={minPanelSize}
48+
maxPanelSize={actualMaxPanelSize}
49+
resizable={true}
50+
onPanelResize={({ detail }) => setSize(detail.panelSize)}
51+
onLayoutChange={({ detail }) => setActualMaxPanelSize(Math.min(detail.totalSize - minContentSize, maxPanelSize))}
52+
display={display === 'all' && collapsed ? 'main-only' : display}
53+
panelPosition={panelPosition}
54+
mainFocusable={longMainContent && !buttons ? { ariaLabel: 'Main content' } : undefined}
55+
panelFocusable={longPanelContent && !buttons ? { ariaLabel: 'Panel content' } : undefined}
56+
panelContent={
57+
<Box padding="m">
58+
<Header>Panel content</Header>
59+
{new Array(longPanelContent ? 20 : 1)
60+
.fill('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')
61+
.map((t, i) => (
62+
<div key={i}>{t}</div>
63+
))}
64+
{buttons && <Button>Button</Button>}
65+
</Box>
66+
}
67+
mainContent={
68+
<Box padding="m">
69+
<Header>Main content{collapsed && ' [collapsed]'}</Header>
70+
{new Array(longMainContent ? 200 : 1)
71+
.fill('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')
72+
.map((t, i) => (
73+
<div key={i}>{t}</div>
74+
))}
75+
{buttons && <Button>button</Button>}
76+
</Box>
77+
}
78+
/>
9179
);
9280
};
9381

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18004,6 +18004,29 @@ exports[`Components definition for panel-layout matches the snapshot: panel-layo
1800418004
{
1800518005
"dashCaseName": "panel-layout",
1800618006
"events": [
18007+
{
18008+
"cancelable": false,
18009+
"description": "Called when the panel and/or main content size changes. This can be due
18010+
to user resizing or changes to the available space on the page.",
18011+
"detailInlineType": {
18012+
"name": "PanelLayoutProps.PanelResizeDetail",
18013+
"properties": [
18014+
{
18015+
"name": "panelSize",
18016+
"optional": false,
18017+
"type": "number",
18018+
},
18019+
{
18020+
"name": "totalSize",
18021+
"optional": false,
18022+
"type": "number",
18023+
},
18024+
],
18025+
"type": "object",
18026+
},
18027+
"detailType": "PanelLayoutProps.PanelResizeDetail",
18028+
"name": "onLayoutChange",
18029+
},
1800718030
{
1800818031
"cancelable": false,
1800918032
"description": "Called when the user resizes the panel.",

src/panel-layout/__tests__/panel-layout.test.tsx

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
import React from 'react';
4-
import { render } from '@testing-library/react';
4+
import { act, render } from '@testing-library/react';
55

66
import { useResize } from '../../../lib/components/app-layout/visual-refresh-toolbar/drawer/use-resize';
77
import useContainerWidth from '../../../lib/components/internal/utils/use-container-width';
@@ -633,4 +633,176 @@ describe('PanelLayout Component', () => {
633633
expect(content).not.toHaveAttribute('aria-labelledby');
634634
});
635635
});
636+
637+
describe('onLayoutChange', () => {
638+
test('is called when container width changes', () => {
639+
const onLayoutChange = jest.fn();
640+
641+
const { rerender } = render(
642+
<PanelLayout panelContent="Panel" mainContent="Main" onLayoutChange={onLayoutChange} />
643+
);
644+
645+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
646+
onLayoutChange.mockClear();
647+
648+
// Simulate container width change
649+
mockUseContainerWidth.mockReturnValue([1000, React.createRef()]);
650+
rerender(<PanelLayout panelContent="Panel" mainContent="Main" onLayoutChange={onLayoutChange} />);
651+
652+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
653+
expect(onLayoutChange).toHaveBeenCalledWith(
654+
expect.objectContaining({
655+
detail: { totalSize: 1000, panelSize: 200 },
656+
})
657+
);
658+
});
659+
660+
test('is called when panelSize prop changes in controlled mode', () => {
661+
const onLayoutChange = jest.fn();
662+
const onPanelResize = jest.fn();
663+
664+
const { rerender } = render(
665+
<PanelLayout
666+
panelContent="Panel"
667+
mainContent="Main"
668+
panelSize={300}
669+
onPanelResize={onPanelResize}
670+
onLayoutChange={onLayoutChange}
671+
/>
672+
);
673+
674+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
675+
expect(onLayoutChange).toHaveBeenCalledWith(
676+
expect.objectContaining({
677+
detail: { totalSize: CONTAINER_WIDTH, panelSize: 300 },
678+
})
679+
);
680+
onLayoutChange.mockClear();
681+
682+
// Change panelSize prop
683+
rerender(
684+
<PanelLayout
685+
panelContent="Panel"
686+
mainContent="Main"
687+
panelSize={400}
688+
onPanelResize={onPanelResize}
689+
onLayoutChange={onLayoutChange}
690+
/>
691+
);
692+
693+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
694+
expect(onLayoutChange).toHaveBeenCalledWith(
695+
expect.objectContaining({
696+
detail: { totalSize: CONTAINER_WIDTH, panelSize: 400 },
697+
})
698+
);
699+
});
700+
701+
test('is called when user resizes the panel', () => {
702+
const onLayoutChange = jest.fn();
703+
let newPanelSize = 300;
704+
const onPanelResize = jest.fn(event => {
705+
newPanelSize = event.detail.panelSize;
706+
});
707+
let capturedOnResize: ((size: number) => void) | undefined;
708+
709+
mockUseResize.mockImplementation(({ onResize }) => {
710+
capturedOnResize = onResize;
711+
return {
712+
onKeyDown: jest.fn(),
713+
onDirectionClick: jest.fn(),
714+
onPointerDown: jest.fn(),
715+
relativeSize: 50,
716+
};
717+
});
718+
719+
const { rerender } = render(
720+
<PanelLayout
721+
panelContent="Panel"
722+
mainContent="Main"
723+
resizable={true}
724+
panelSize={newPanelSize}
725+
onPanelResize={onPanelResize}
726+
onLayoutChange={onLayoutChange}
727+
/>
728+
);
729+
730+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
731+
onLayoutChange.mockClear();
732+
733+
// Simulate user resize - this triggers onPanelResize
734+
act(() => {
735+
capturedOnResize!(350);
736+
});
737+
738+
// In controlled mode, parent would update panelSize prop
739+
rerender(
740+
<PanelLayout
741+
panelContent="Panel"
742+
mainContent="Main"
743+
resizable={true}
744+
panelSize={newPanelSize}
745+
onPanelResize={onPanelResize}
746+
onLayoutChange={onLayoutChange}
747+
/>
748+
);
749+
750+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
751+
expect(onLayoutChange).toHaveBeenCalledWith(
752+
expect.objectContaining({
753+
detail: { totalSize: CONTAINER_WIDTH, panelSize: 350 },
754+
})
755+
);
756+
});
757+
758+
test('is called with clamped panel size when below minPanelSize', () => {
759+
const onLayoutChange = jest.fn();
760+
761+
renderPanelLayout({
762+
panelSize: 50,
763+
minPanelSize: 150,
764+
onPanelResize: () => {},
765+
onLayoutChange,
766+
});
767+
768+
expect(onLayoutChange).toHaveBeenCalledWith(
769+
expect.objectContaining({
770+
detail: { totalSize: CONTAINER_WIDTH, panelSize: 150 },
771+
})
772+
);
773+
});
774+
775+
test('is called with clamped panel size when above maxPanelSize', () => {
776+
const onLayoutChange = jest.fn();
777+
778+
renderPanelLayout({
779+
panelSize: 600,
780+
maxPanelSize: 400,
781+
onPanelResize: () => {},
782+
onLayoutChange,
783+
});
784+
785+
expect(onLayoutChange).toHaveBeenCalledWith(
786+
expect.objectContaining({
787+
detail: { totalSize: CONTAINER_WIDTH, panelSize: 400 },
788+
})
789+
);
790+
});
791+
792+
test('is called with clamped panel size when above container width', () => {
793+
const onLayoutChange = jest.fn();
794+
795+
renderPanelLayout({
796+
panelSize: 1000,
797+
onPanelResize: () => {},
798+
onLayoutChange,
799+
});
800+
801+
expect(onLayoutChange).toHaveBeenCalledWith(
802+
expect.objectContaining({
803+
detail: { totalSize: CONTAINER_WIDTH, panelSize: CONTAINER_WIDTH },
804+
})
805+
);
806+
});
807+
});
636808
});

src/panel-layout/interfaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ export interface PanelLayoutProps extends BaseComponentProps {
8585
* Called when the user resizes the panel.
8686
*/
8787
onPanelResize?: NonCancelableEventHandler<PanelLayoutProps.PanelResizeDetail>;
88+
89+
/**
90+
* Called when the panel and/or main content size changes. This can be due
91+
* to user resizing or changes to the available space on the page.
92+
*/
93+
onLayoutChange?: NonCancelableEventHandler<PanelLayoutProps.PanelResizeDetail>;
8894
}
8995

9096
export namespace PanelLayoutProps {

0 commit comments

Comments
 (0)