Skip to content

Commit 191a662

Browse files
fix: Fix AppLayoutToolbar breadcrumbs SSR glitch (#3856)
Co-authored-by: Joan Perals <[email protected]>
1 parent 9e0a7f3 commit 191a662

File tree

7 files changed

+174
-26
lines changed

7 files changed

+174
-26
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
import { render } from '@testing-library/react';
5+
6+
import { AppLayoutInternals } from '../../../../../lib/components/app-layout/visual-refresh-toolbar/interfaces';
7+
import { ToolbarSkeleton } from '../../../../../lib/components/app-layout/visual-refresh-toolbar/skeleton/skeleton-parts';
8+
import { ToolbarProps } from '../../../../../lib/components/app-layout/visual-refresh-toolbar/toolbar';
9+
import createWrapper from '../../../../../lib/components/test-utils/dom';
10+
11+
describe('ToolbarSkeleton', () => {
12+
test('renders with discoveredBreadcrumbs', () => {
13+
const { container } = render(
14+
<ToolbarSkeleton
15+
appLayoutInternals={
16+
{
17+
breadcrumbs: null,
18+
discoveredBreadcrumbs: {
19+
items: [
20+
{ text: 'Home', href: '/home' },
21+
{ text: 'Page', href: '/page' },
22+
],
23+
},
24+
} as Partial<AppLayoutInternals> as AppLayoutInternals
25+
}
26+
toolbarProps={{} as ToolbarProps}
27+
/>
28+
);
29+
30+
const wrapper = createWrapper(container);
31+
const breadcrumbGroups = wrapper.findAllBreadcrumbGroups();
32+
33+
expect(breadcrumbGroups).toHaveLength(1);
34+
35+
const breadcrumbLinks = breadcrumbGroups[0].findBreadcrumbLinks();
36+
expect(breadcrumbLinks).toHaveLength(2);
37+
expect(breadcrumbLinks[0].getElement().textContent).toEqual('Home');
38+
expect(breadcrumbLinks[1].getElement().textContent).toEqual('Page');
39+
});
40+
41+
test('renders with own breadcrumbs', () => {
42+
const { container } = render(
43+
<ToolbarSkeleton
44+
appLayoutInternals={
45+
{
46+
breadcrumbs: <div data-testid="custom-breadcrumbs">Custom Breadcrumbs</div>,
47+
discoveredBreadcrumbs: null,
48+
} as Partial<AppLayoutInternals> as AppLayoutInternals
49+
}
50+
toolbarProps={{} as ToolbarProps}
51+
/>
52+
);
53+
54+
expect(container.querySelector('[data-testid="custom-breadcrumbs"]')).toBeTruthy();
55+
expect(container.querySelector('[data-testid="custom-breadcrumbs"]')?.textContent).toEqual('Custom Breadcrumbs');
56+
});
57+
58+
test('renders without breadcrumbs', () => {
59+
const { container } = render(
60+
<ToolbarSkeleton
61+
appLayoutInternals={
62+
{
63+
breadcrumbs: null,
64+
discoveredBreadcrumbs: null,
65+
} as Partial<AppLayoutInternals> as AppLayoutInternals
66+
}
67+
toolbarProps={{} as ToolbarProps}
68+
/>
69+
);
70+
71+
const wrapper = createWrapper(container);
72+
const breadcrumbGroups = wrapper.findAllBreadcrumbGroups();
73+
74+
expect(breadcrumbGroups).toHaveLength(0);
75+
});
76+
});

src/app-layout/visual-refresh-toolbar/skeleton/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
AppLayoutBottomContentSlot,
1616
AppLayoutTopContentSlot,
1717
} from '../internal';
18+
import { isWidgetReady } from '../state/invariants';
1819
import { ToolbarProps } from '../toolbar';
1920
import { SkeletonPartProps, SkeletonSlotsAttributes } from './interfaces';
2021

@@ -58,12 +59,14 @@ export const SkeletonLayout = ({
5859
contentElAttributes,
5960
} = skeletonSlotsAttributes;
6061

62+
const isWidgetLoaded = isWidgetReady(appLayoutState);
63+
6164
return (
6265
<VisualContext contextName="app-layout-toolbar">
6366
<div
6467
{...getAnalyticsMetadataAttribute({ component: componentAnalyticsMetadata })}
6568
ref={appLayoutState.rootRef as React.Ref<HTMLDivElement>}
66-
data-awsui-app-layout-widget-loaded={false}
69+
data-awsui-app-layout-widget-loaded={isWidgetLoaded}
6770
{...wrapperElAttributes}
6871
className={wrapperElAttributes?.className ?? clsx(styles.root, testutilStyles.root)}
6972
style={

src/app-layout/visual-refresh-toolbar/skeleton/skeleton-parts.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import React from 'react';
55
import { AppLayoutNotificationsImplementationProps } from '../notifications';
66
import { AppLayoutToolbarImplementationProps } from '../toolbar';
77
import { SkeletonPartProps } from './interfaces';
8-
import { BreadcrumbsSlot, NotificationsSlot, ToolbarSlot } from './slots';
8+
import { NotificationsSlot } from './slots';
9+
import { ToolbarSkeletonStructure } from './toolbar-container';
910

1011
import styles from './styles.css.js';
1112

@@ -17,11 +18,7 @@ export const BeforeMainSlotSkeleton = React.forwardRef<HTMLElement, SkeletonPart
1718
({ toolbarProps, appLayoutProps }, ref) => {
1819
return (
1920
<>
20-
{!!toolbarProps && (
21-
<ToolbarSlot ref={ref}>
22-
<BreadcrumbsSlot ownBreadcrumbs={appLayoutProps.breadcrumbs} />
23-
</ToolbarSlot>
24-
)}
21+
{!!toolbarProps && <ToolbarSkeletonStructure ref={ref} ownBreadcrumbs={appLayoutProps.breadcrumbs} />}
2522
{toolbarProps?.navigationOpen && <div className={styles.navigation} />}
2623
</>
2724
);
@@ -34,12 +31,11 @@ export const BeforeMainSlotSkeleton = React.forwardRef<HTMLElement, SkeletonPart
3431

3532
export const ToolbarSkeleton = React.forwardRef<HTMLElement, AppLayoutToolbarImplementationProps>(
3633
({ appLayoutInternals }: AppLayoutToolbarImplementationProps, ref) => (
37-
<ToolbarSlot ref={ref}>
38-
<BreadcrumbsSlot
39-
ownBreadcrumbs={appLayoutInternals.breadcrumbs}
40-
discoveredBreadcrumbs={appLayoutInternals.discoveredBreadcrumbs}
41-
/>
42-
</ToolbarSlot>
34+
<ToolbarSkeletonStructure
35+
ref={ref}
36+
ownBreadcrumbs={appLayoutInternals.breadcrumbs}
37+
discoveredBreadcrumbs={appLayoutInternals.discoveredBreadcrumbs}
38+
/>
4339
)
4440
);
4541

src/app-layout/visual-refresh-toolbar/skeleton/slots.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ interface ToolbarSlotProps {
1616
}
1717

1818
export const ToolbarSlot = React.forwardRef<HTMLElement, ToolbarSlotProps>(({ className, style, children }, ref) => (
19-
<section ref={ref as React.Ref<any>} className={clsx(styles['toolbar-container'], className)} style={style}>
19+
<section
20+
ref={ref as React.Ref<any>}
21+
className={clsx(styles['toolbar-container'], className)}
22+
style={{
23+
insetBlockStart: style?.insetBlockStart ?? 0,
24+
...style,
25+
}}
26+
>
2027
{children}
2128
</section>
2229
));
@@ -40,11 +47,15 @@ interface BreadcrumbsSlotProps {
4047
discoveredBreadcrumbs?: BreadcrumbGroupProps | null;
4148
}
4249

50+
const breadcrumbsSlotContextValue = { isInToolbar: true };
51+
4352
export function BreadcrumbsSlot({ ownBreadcrumbs, discoveredBreadcrumbs }: BreadcrumbsSlotProps) {
53+
const isSSR = typeof window === 'undefined';
54+
4455
return (
45-
<BreadcrumbsSlotContext.Provider value={{ isInToolbar: true }}>
56+
<BreadcrumbsSlotContext.Provider value={breadcrumbsSlotContextValue}>
4657
<div className={styles['breadcrumbs-own']}>{ownBreadcrumbs}</div>
47-
{discoveredBreadcrumbs && (
58+
{discoveredBreadcrumbs && !isSSR && (
4859
<div className={styles['breadcrumbs-discovered']}>
4960
<BreadcrumbGroupImplementation
5061
{...discoveredBreadcrumbs}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
import clsx from 'clsx';
5+
6+
import { BreadcrumbGroupProps } from '../../../breadcrumb-group/interfaces';
7+
import { BreadcrumbsSlot, ToolbarSlot } from './slots';
8+
9+
import testutilStyles from '../../test-classes/styles.css.js';
10+
import toolbarStyles from '../toolbar/styles.css.js';
11+
12+
interface ToolbarContainerProps {
13+
children: React.ReactNode;
14+
hasAiDrawer?: boolean;
15+
}
16+
17+
export function ToolbarContainer({ children, hasAiDrawer }: ToolbarContainerProps) {
18+
return (
19+
<div className={clsx(toolbarStyles['toolbar-container'], hasAiDrawer && toolbarStyles['with-ai-drawer'])}>
20+
{children}
21+
</div>
22+
);
23+
}
24+
25+
interface ToolbarBreadcrumbsSectionProps {
26+
ownBreadcrumbs: React.ReactNode;
27+
discoveredBreadcrumbs?: BreadcrumbGroupProps | null;
28+
includeTestUtils?: boolean;
29+
}
30+
31+
export function ToolbarBreadcrumbsSection({
32+
ownBreadcrumbs,
33+
discoveredBreadcrumbs,
34+
includeTestUtils = false,
35+
}: ToolbarBreadcrumbsSectionProps) {
36+
return (
37+
<div
38+
className={clsx(toolbarStyles['universal-toolbar-breadcrumbs'], includeTestUtils && testutilStyles.breadcrumbs)}
39+
>
40+
<BreadcrumbsSlot ownBreadcrumbs={ownBreadcrumbs} discoveredBreadcrumbs={discoveredBreadcrumbs} />
41+
</div>
42+
);
43+
}
44+
45+
interface ToolbarSkeletonStructureProps {
46+
ownBreadcrumbs: React.ReactNode;
47+
discoveredBreadcrumbs?: BreadcrumbGroupProps | null;
48+
}
49+
50+
export const ToolbarSkeletonStructure = React.forwardRef<HTMLElement, ToolbarSkeletonStructureProps>(
51+
({ ownBreadcrumbs, discoveredBreadcrumbs }, ref) => (
52+
<ToolbarSlot ref={ref}>
53+
<ToolbarContainer>
54+
<ToolbarBreadcrumbsSection ownBreadcrumbs={ownBreadcrumbs} discoveredBreadcrumbs={discoveredBreadcrumbs} />
55+
<div className={toolbarStyles['universal-toolbar-drawers']} />
56+
</ToolbarContainer>
57+
</ToolbarSlot>
58+
)
59+
);

src/app-layout/visual-refresh-toolbar/state/use-skeleton-slots-attributes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export const useSkeletonSlotsAttributes = (
5353
[customCssProps.navigationWidth]: `${navigationWidth}px`,
5454
[customCssProps.toolsWidth]: `${activeDrawerSize}px`,
5555
},
56-
'data-awsui-app-layout-widget-loaded': true,
5756
};
5857

5958
const mainElAttributes = {

src/app-layout/visual-refresh-toolbar/toolbar/index.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import clsx from 'clsx';
66

77
import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal';
88

9+
import { createWidgetizedComponent } from '../../../internal/widgets';
910
import { AppLayoutProps } from '../../interfaces';
1011
import { OnChangeParams } from '../../utils/use-drawers';
1112
import { Focusable, FocusControlMultipleStates } from '../../utils/use-focus-control';
1213
import { AppLayoutInternals } from '../interfaces';
13-
import { BreadcrumbsSlot, ToolbarSlot } from '../skeleton/slots';
14+
import { ToolbarSkeleton } from '../skeleton/skeleton-parts';
15+
import { ToolbarSlot } from '../skeleton/slots';
16+
import { ToolbarBreadcrumbsSection, ToolbarContainer } from '../skeleton/toolbar-container';
1417
import { DrawerTriggers, SplitPanelToggleProps } from './drawer-triggers';
1518
import TriggerButton from './trigger-button';
1619

@@ -197,7 +200,7 @@ export function AppLayoutToolbarImplementation({
197200
</div>
198201
)}
199202
</Transition>
200-
<div className={clsx(styles['toolbar-container'], !!aiDrawer?.trigger && styles['with-ai-drawer'])}>
203+
<ToolbarContainer hasAiDrawer={!!aiDrawer?.trigger}>
201204
{hasNavigation && (
202205
<nav {...navLandmarkAttributes} className={clsx(styles['universal-toolbar-nav'])}>
203206
<TriggerButton
@@ -221,12 +224,11 @@ export function AppLayoutToolbarImplementation({
221224
</nav>
222225
)}
223226
{(breadcrumbs || discoveredBreadcrumbs) && (
224-
<div className={clsx(styles['universal-toolbar-breadcrumbs'], testutilStyles.breadcrumbs)}>
225-
<BreadcrumbsSlot
226-
ownBreadcrumbs={appLayoutInternals.breadcrumbs}
227-
discoveredBreadcrumbs={appLayoutInternals.discoveredBreadcrumbs}
228-
/>
229-
</div>
227+
<ToolbarBreadcrumbsSection
228+
ownBreadcrumbs={appLayoutInternals.breadcrumbs}
229+
discoveredBreadcrumbs={appLayoutInternals.discoveredBreadcrumbs}
230+
includeTestUtils={true}
231+
/>
230232
)}
231233
{(drawers?.length ||
232234
globalDrawers?.length ||
@@ -256,7 +258,9 @@ export function AppLayoutToolbarImplementation({
256258
/>
257259
</div>
258260
)}
259-
</div>
261+
</ToolbarContainer>
260262
</ToolbarSlot>
261263
);
262264
}
265+
266+
export const AppLayoutToolbar = createWidgetizedComponent(AppLayoutToolbarImplementation, ToolbarSkeleton);

0 commit comments

Comments
 (0)