Skip to content

Commit 0dfe633

Browse files
georgylobkojust-boris
authored andcommitted
feat: Widgetise app layout skeleton
chore: Move skeleton elements attributes to a widgetized hook chore: Move skeleton slots to widgets chore: Widgetize all remaining components feat: AppLayout state as widget chore: small fix chore: small fix chore: Small refactoring chore: Pull react-reverse-portal as src util chore: Revert unrelated changes in runtime-drawers.page.tsx fix: SSR tests fix: Skeleton part tests feat: Deliver skeleton slots attributes as a widget part fix: Unit tests chore: Update snapshots chore: Tests for reverse portal chore: Additional tests for reverse portal fix: Integ tests chore: Skip hash for class names in widget contract tests fix: Tests chore: Wrap up chore: Fallbacks for app layout component when is in loading state (avoiding layout cumulative shifting) chore: Delay breadcrumbs rendering until app layout state is loaded feat: Event-base prop passing Fix u tests and remove unused reverse-portal feat: Split app layout state into widgetized and built-in parts chore: Merge expanded mode chore: Merge from main fix: useMergeRefs import fix: remove unnecessary import fix: Navigation fallback for useMultiAppLayout chore: Refactoring, fixed tools test chore: Refactoring, fixed skeleton test chore: Update snapshots fix: header-variant test fix: toolbar test fix: multi-layout.test.tsx fix: slots.test.tsx fix: desktop.test.tsx chore: Update snapshots fix: split-panel.test.tsx fix: main.test.tsx fix: drawers.test.tsx fix: common.test.tsx fix: analytics-metadata.test.tsx chore: Update snapshots chore: Update snapshots feat: Async focus control chore: Migrate useUniqueId import to @cloudscape-design/component-toolkit fix: onMount type issue feat: Introduce useAsyncFocusControl chore: Clean up style hash in widget-contract-split-panel.test.tsx chore: Cleanup multi-layout-with-hidden-instances-iframe.page.tsx chore: Small test adjustment chore: Suppress an overlay for runtime errors in webpack config fix: Eslint error fix: AppLayoutPartLoader mounting issue chore: Increased a timeout for multi-layout.test.tsx chore: Types for a state manager chore: Wrapping up (type adjustments, snapshot upd) chore: Turn focusSplitPanel into async function (wait for an element before focusing on it) feat: Custom feature flag appLayoutDelayedWidget to test async widget behavior chore: Integ test for checking ensure the layout is not shifting after widget loading chore: Upd snapshot tests fix: Duplicate 'client' key in webpack.config.base.cjs fix: analytics metadata
1 parent d12aac5 commit 0dfe633

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+4167
-4073
lines changed

pages/app-layout/runtime-drawers.page.tsx

Lines changed: 86 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import React, { useContext, useRef, useState } from 'react';
3+
import React, { useContext, useEffect, useRef, useState } from 'react';
44

55
import {
66
AppLayout,
@@ -31,6 +31,18 @@ type DemoContext = React.Context<
3131
}>
3232
>;
3333

34+
const CustomContent = () => {
35+
useEffect(() => {
36+
console.log('mount');
37+
38+
return () => {
39+
console.log('unmount');
40+
};
41+
}, []);
42+
43+
return <div>Custom content</div>;
44+
};
45+
3446
export default function WithDrawers() {
3547
const [activeDrawerId, setActiveDrawerId] = useState<string | null>(null);
3648
const [helpPathSlug, setHelpPathSlug] = useState<string>('default');
@@ -70,83 +82,86 @@ export default function WithDrawers() {
7082
breadcrumbs={<Breadcrumbs />}
7183
ref={appLayoutRef}
7284
content={
73-
<ContentLayout
74-
disableOverlap={true}
75-
header={
76-
<SpaceBetween size="m">
77-
<Header
78-
variant="h1"
79-
description="Sometimes you need custom drawers to get the job done."
80-
info={
81-
<Link
82-
data-testid="info-link-header"
83-
variant="info"
84-
onFollow={() => {
85-
setHelpPathSlug('header');
86-
setIsToolsOpen(true);
87-
appLayoutRef.current?.focusToolsClose();
88-
}}
89-
>
90-
Info
91-
</Link>
92-
}
93-
>
94-
Testing Custom Drawers!
95-
</Header>
85+
<div data-testid="app-layout-content-area">
86+
<ContentLayout
87+
disableOverlap={true}
88+
header={
89+
<SpaceBetween size="m">
90+
<Header
91+
variant="h1"
92+
description="Sometimes you need custom drawers to get the job done."
93+
info={
94+
<Link
95+
data-testid="info-link-header"
96+
variant="info"
97+
onFollow={() => {
98+
setHelpPathSlug('header');
99+
setIsToolsOpen(true);
100+
appLayoutRef.current?.focusToolsClose();
101+
}}
102+
>
103+
Info
104+
</Link>
105+
}
106+
>
107+
Testing Custom Drawers!
108+
</Header>
96109

97-
<SpaceBetween size="xs">
98-
<Toggle checked={hasTools} onChange={({ detail }) => setUrlParams({ hasTools: detail.checked })}>
99-
Use Tools
100-
</Toggle>
110+
<SpaceBetween size="xs">
111+
<Toggle checked={hasTools} onChange={({ detail }) => setUrlParams({ hasTools: detail.checked })}>
112+
Use Tools
113+
</Toggle>
101114

102-
<Toggle checked={hasDrawers} onChange={({ detail }) => setUrlParams({ hasDrawers: detail.checked })}>
103-
Use Drawers
104-
</Toggle>
115+
<Toggle checked={hasDrawers} onChange={({ detail }) => setUrlParams({ hasDrawers: detail.checked })}>
116+
Use Drawers
117+
</Toggle>
105118

106-
<Button
107-
onClick={() => awsuiPlugins.appLayout.openDrawer('circle4-global')}
108-
data-testid="open-drawer-button"
109-
>
110-
Open a drawer without a trigger
111-
</Button>
112-
<Button onClick={() => awsuiPlugins.appLayout.closeDrawer('circle4-global')}>
113-
Close a drawer without a trigger
114-
</Button>
119+
<Button
120+
onClick={() => awsuiPlugins.appLayout.openDrawer('circle4-global')}
121+
data-testid="open-drawer-button"
122+
>
123+
Open a drawer without a trigger
124+
</Button>
125+
<Button onClick={() => awsuiPlugins.appLayout.closeDrawer('circle4-global')}>
126+
Close a drawer without a trigger
127+
</Button>
115128

116-
<Button
117-
onClick={() => awsuiPlugins.appLayout.resizeDrawer('circle-global', 400)}
118-
data-testid="button-circle-global-resize"
119-
>
120-
Resize circle-global drawer to 400px
121-
</Button>
122-
<Button
123-
onClick={() => awsuiPlugins.appLayout.resizeDrawer('circle3-global', 500)}
124-
data-testid="button-circle3-global-resize"
125-
>
126-
Resize circle3-global drawer to 500px
127-
</Button>
129+
<Button
130+
onClick={() => awsuiPlugins.appLayout.resizeDrawer('circle-global', 400)}
131+
data-testid="button-circle-global-resize"
132+
>
133+
Resize circle-global drawer to 400px
134+
</Button>
135+
<Button
136+
onClick={() => awsuiPlugins.appLayout.resizeDrawer('circle3-global', 500)}
137+
data-testid="button-circle3-global-resize"
138+
>
139+
Resize circle3-global drawer to 500px
140+
</Button>
141+
</SpaceBetween>
128142
</SpaceBetween>
129-
</SpaceBetween>
130-
}
131-
>
132-
<Header
133-
info={
134-
<Link
135-
data-testid="info-link-content"
136-
variant="info"
137-
onFollow={() => {
138-
setHelpPathSlug('content');
139-
setIsToolsOpen(true);
140-
}}
141-
>
142-
Info
143-
</Link>
144143
}
145144
>
146-
Content
147-
</Header>
148-
<Containers />
149-
</ContentLayout>
145+
<Header
146+
info={
147+
<Link
148+
data-testid="info-link-content"
149+
variant="info"
150+
onFollow={() => {
151+
setHelpPathSlug('content');
152+
setIsToolsOpen(true);
153+
}}
154+
>
155+
Info
156+
</Link>
157+
}
158+
>
159+
Content
160+
</Header>
161+
<CustomContent />
162+
<Containers />
163+
</ContentLayout>
164+
</div>
150165
}
151166
splitPanel={
152167
<SplitPanel header="Split panel header" i18nStrings={splitPaneli18nStrings}>

pages/app/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@ interface GlobalFlags {
2222
appLayoutWidget?: boolean;
2323
appLayoutToolbar?: boolean;
2424
}
25+
// used for local dev / testing
26+
interface CustomFlags {
27+
appLayoutDelayedWidget?: boolean;
28+
}
2529
const awsuiVisualRefreshFlag = Symbol.for('awsui-visual-refresh-flag');
2630
const awsuiGlobalFlagsSymbol = Symbol.for('awsui-global-flags');
31+
const awsuiCustomFlagsSymbol = Symbol.for('awsui-custom-flags');
2732

2833
interface ExtendedWindow extends Window {
2934
[awsuiVisualRefreshFlag]?: () => boolean;
3035
[awsuiGlobalFlagsSymbol]?: GlobalFlags;
36+
[awsuiCustomFlagsSymbol]?: CustomFlags;
3137
}
3238
declare const window: ExtendedWindow;
3339

@@ -86,15 +92,21 @@ function App() {
8692
}
8793

8894
const history = createHashHistory();
89-
const { direction, visualRefresh, appLayoutWidget, appLayoutToolbar } = parseQuery(history.location.search);
95+
const { direction, visualRefresh, appLayoutWidget, appLayoutToolbar, appLayoutDelayedWidget } = parseQuery(
96+
history.location.search
97+
);
9098

9199
// The VR class needs to be set before any React rendering occurs.
92100
window[awsuiVisualRefreshFlag] = () => visualRefresh;
93101
if (!window[awsuiGlobalFlagsSymbol]) {
94102
window[awsuiGlobalFlagsSymbol] = {};
95103
}
104+
if (!window[awsuiCustomFlagsSymbol]) {
105+
window[awsuiCustomFlagsSymbol] = {};
106+
}
96107
window[awsuiGlobalFlagsSymbol].appLayoutWidget = appLayoutWidget;
97108
window[awsuiGlobalFlagsSymbol].appLayoutToolbar = appLayoutToolbar;
109+
window[awsuiCustomFlagsSymbol].appLayoutDelayedWidget = appLayoutDelayedWidget;
98110

99111
// Apply the direction value to the HTML element dir attribute
100112
document.documentElement.setAttribute('dir', direction);

pages/utils/iframe-wrapper.tsx

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,37 @@ export function IframeWrapper({ id, AppComponent }: { id: string; AppComponent:
3232
const ref = useRef<HTMLDivElement>(null);
3333

3434
useEffect(() => {
35-
const container = ref.current;
36-
if (!container) {
37-
return;
38-
}
39-
const iframeEl = container.ownerDocument.createElement('iframe');
40-
iframeEl.className = styles['full-screen'];
41-
iframeEl.id = id;
42-
iframeEl.title = id;
43-
container.appendChild(iframeEl);
35+
setTimeout(() => {
36+
const container = ref.current;
37+
if (!container) {
38+
return;
39+
}
40+
const iframeEl = container.ownerDocument.createElement('iframe');
41+
iframeEl.className = styles['full-screen'];
42+
iframeEl.id = id;
43+
iframeEl.title = id;
44+
container.appendChild(iframeEl);
4445

45-
const iframeDocument = iframeEl.contentDocument!;
46-
// Prevent iframe document instance from reload
47-
// https://bugzilla.mozilla.org/show_bug.cgi?id=543435
48-
iframeDocument.open();
49-
// set html5 doctype
50-
iframeDocument.writeln('<!DOCTYPE html>');
51-
iframeDocument.close();
46+
const iframeDocument = iframeEl.contentDocument!;
47+
// Prevent iframe document instance from reload
48+
// https://bugzilla.mozilla.org/show_bug.cgi?id=543435
49+
iframeDocument.open();
50+
// set html5 doctype
51+
iframeDocument.writeln('<!DOCTYPE html>');
52+
iframeDocument.close();
5253

53-
const innerAppRoot = iframeDocument.createElement('div');
54-
iframeDocument.body.appendChild(innerAppRoot);
55-
copyStyles(document, iframeDocument);
56-
iframeDocument.dir = document.dir;
57-
const syncClassesCleanup = syncClasses(document.body, iframeDocument.body);
58-
ReactDOM.render(<AppComponent />, innerAppRoot);
59-
return () => {
60-
syncClassesCleanup();
61-
ReactDOM.unmountComponentAtNode(innerAppRoot);
62-
container.removeChild(iframeEl);
63-
};
54+
const innerAppRoot = iframeDocument.createElement('div');
55+
iframeDocument.body.appendChild(innerAppRoot);
56+
copyStyles(document, iframeDocument);
57+
iframeDocument.dir = document.dir;
58+
const syncClassesCleanup = syncClasses(document.body, iframeDocument.body);
59+
ReactDOM.render(<AppComponent />, innerAppRoot);
60+
return () => {
61+
syncClassesCleanup();
62+
ReactDOM.unmountComponentAtNode(innerAppRoot);
63+
container.removeChild(iframeEl);
64+
};
65+
}, 50);
6466
}, [id, AppComponent]);
6567

6668
return <div ref={ref}></div>;

src/app-layout-toolbar/__tests__/analytics-metadata.test.tsx

Lines changed: 21 additions & 6 deletions
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 { act, render } from '@testing-library/react';
4+
import { act, render, waitFor } from '@testing-library/react';
55

66
import {
77
activateAnalyticsMetadata,
@@ -125,7 +125,7 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
125125
});
126126
});
127127
describe('with tools', () => {
128-
test('closed', () => {
128+
test('closed', async () => {
129129
const wrapper = renderToolbar({
130130
tools: <span>tools</span>,
131131
toolsOpen: false,
@@ -134,6 +134,9 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
134134
toolsToggle: 'toggle tools',
135135
},
136136
});
137+
await waitFor(() => {
138+
expect(wrapper.findToolsToggle()).toBeTruthy();
139+
});
137140
const toolsTrigger = wrapper.findToolsToggle().getElement();
138141
validateComponentNameAndLabels(toolsTrigger, {});
139142
expect(getGeneratedAnalyticsMetadata(toolsTrigger)).toEqual({
@@ -144,7 +147,7 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
144147
...getMetadata(),
145148
});
146149
});
147-
test('open', () => {
150+
test('open', async () => {
148151
const wrapper = renderToolbar({
149152
tools: <span>tools</span>,
150153
toolsOpen: true,
@@ -154,6 +157,9 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
154157
toolsClose: 'close tools',
155158
},
156159
});
160+
await waitFor(() => {
161+
expect(wrapper.findToolsToggle()).toBeTruthy();
162+
});
157163
const toolsTrigger = wrapper.findToolsToggle().getElement();
158164
validateComponentNameAndLabels(toolsTrigger, {});
159165
expect(getGeneratedAnalyticsMetadata(toolsTrigger)).toEqual({
@@ -174,7 +180,7 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
174180
});
175181

176182
describe('with local drawer', () => {
177-
test('closed', () => {
183+
test('closed', async () => {
178184
const wrapper = renderToolbar({
179185
drawers: [
180186
{
@@ -191,6 +197,9 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
191197
},
192198
],
193199
});
200+
await waitFor(() => {
201+
expect(wrapper.findDrawerTriggerById('test-drawer')).toBeTruthy();
202+
});
194203
const drawerTrigger = wrapper.findDrawerTriggerById('test-drawer')!.getElement();
195204
validateComponentNameAndLabels(drawerTrigger, {});
196205
expect(getGeneratedAnalyticsMetadata(drawerTrigger)).toEqual({
@@ -201,7 +210,7 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
201210
...getMetadata(),
202211
});
203212
});
204-
test('open', () => {
213+
test('open', async () => {
205214
const wrapper = renderToolbar({
206215
drawers: [
207216
{
@@ -232,6 +241,9 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
232241
activeDrawerId: 'test-drawer',
233242
onDrawerChange: () => {},
234243
});
244+
await waitFor(() => {
245+
expect(wrapper.findDrawerTriggerById('test-drawer')).toBeTruthy();
246+
});
235247
const drawerTrigger = wrapper.findDrawerTriggerById('test-drawer')!.getElement();
236248
validateComponentNameAndLabels(drawerTrigger, {});
237249
expect(getGeneratedAnalyticsMetadata(drawerTrigger)).toEqual({
@@ -319,7 +331,7 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
319331
});
320332

321333
describe('with split panel', () => {
322-
test.each(['open', 'close'])('%s', action => {
334+
test.each(['open', 'close'])('%s', async action => {
323335
const wrapper = renderToolbar({
324336
splitPanel: (
325337
<SplitPanel i18nStrings={{ openButtonAriaLabel: 'open split panel' }} header="Split panel header">
@@ -329,6 +341,9 @@ describe('AppLayoutToolbar renders correct analytics metadata', () => {
329341
splitPanelOpen: action !== 'open',
330342
onSplitPanelToggle: () => {},
331343
});
344+
await waitFor(() => {
345+
expect(wrapper.findSplitPanelOpenButton()).toBeTruthy();
346+
});
332347
const splitPanelTrigger = wrapper.findSplitPanelOpenButton()!.getElement();
333348
validateComponentNameAndLabels(splitPanelTrigger, {});
334349
expect(getGeneratedAnalyticsMetadata(splitPanelTrigger)).toEqual({

0 commit comments

Comments
 (0)