Skip to content

Commit

Permalink
chore: Render breadcrumbs slot in the skeleton state
Browse files Browse the repository at this point in the history
  • Loading branch information
just-boris committed Nov 8, 2024
1 parent 672e7f8 commit c5a8373
Show file tree
Hide file tree
Showing 24 changed files with 337 additions and 110 deletions.
4 changes: 2 additions & 2 deletions src/app-layout/__tests__/global-breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,11 @@ describeEachAppLayout({ themes: ['refresh-toolbar'], sizes: ['desktop'] }, () =>
<DestructibleLayout />
</>
);
expect(wrapper.find('[data-testid="local-breadcrumbs"]')!.getElement()).toBeEmptyDOMElement();
expect(wrapper.find('[data-testid="local-breadcrumbs"]')!.findBreadcrumbGroup()).toBeFalsy();

wrapper.find('[data-testid="unmount"]')!.click();
await waitFor(() => {
expect(wrapper.find('[data-testid="local-breadcrumbs"]')!.getElement()).not.toBeEmptyDOMElement();
expect(wrapper.find('[data-testid="local-breadcrumbs"]')!.findBreadcrumbGroup()).toBeTruthy();
});
});

Expand Down
75 changes: 75 additions & 0 deletions src/app-layout/__tests__/skeleton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
/* eslint-disable simple-import-sort/imports */
import React from 'react';

import { describeEachAppLayout, renderComponent } from './utils';
import AppLayout from '../../../lib/components/app-layout';
import BreadcrumbGroup from '../../../lib/components/breadcrumb-group';
import { getFunnelKeySelector } from '../../internal/analytics/selectors';

let widgetMockEnabled = false;
function createWidgetizedComponentMock(Implementation: React.ComponentType, Skeleton: React.ComponentType) {
return () => {
return function Widgetized(props: any) {
if (!widgetMockEnabled) {
return <Implementation {...props} />;
}
if (Skeleton) {
return <Skeleton {...props} />;
}
return null;
};
};
}

jest.mock('../../../lib/components/internal/widgets', () => ({
createWidgetizedComponent: createWidgetizedComponentMock,
}));

describeEachAppLayout({ themes: ['refresh-toolbar'] }, () => {
it('renders complete component by default', () => {
const { wrapper } = renderComponent(
<AppLayout
navigation="test nav"
notifications="test notifications"
breadcrumbs={<BreadcrumbGroup items={[{ text: 'Home', href: '' }]} />}
tools="test tools"
/>
);
expect(wrapper.findToolbar()).toBeTruthy();
expect(wrapper.findNavigation()).toBeTruthy();
expect(wrapper.findBreadcrumbs()).toBeTruthy();
expect(wrapper.find(getFunnelKeySelector('funnel-name'))).toBeTruthy();
expect(wrapper.findNotifications()).toBeTruthy();
expect(wrapper.findTools()).toBeTruthy();
expect(wrapper.findContentRegion()).toBeTruthy();
});

describe('in loading state', () => {
beforeEach(() => {
widgetMockEnabled = true;
});
afterEach(() => {
widgetMockEnabled = false;
});

it('renders skeleton parts only', () => {
const { wrapper } = renderComponent(
<AppLayout
navigation="test nav"
notifications="test notifications"
breadcrumbs={<BreadcrumbGroup items={[{ text: 'Home', href: '' }]} />}
tools="test tools"
/>
);
expect(wrapper.findToolbar()).toBeFalsy();
expect(wrapper.findNavigation()).toBeFalsy();
expect(wrapper.findBreadcrumbs()).toBeFalsy();
expect(wrapper.find(getFunnelKeySelector('funnel-name'))).toBeTruthy();
expect(wrapper.findNotifications()).toBeFalsy();
expect(wrapper.findTools()).toBeFalsy();
expect(wrapper.findContentRegion()).toBeTruthy();
});
});
});
6 changes: 5 additions & 1 deletion src/app-layout/visual-refresh-toolbar/navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { InternalButton } from '../../../button/internal';
import { createWidgetizedComponent } from '../../../internal/widgets';
import { getDrawerStyles } from '../compute-layout';
import { AppLayoutInternals } from '../interfaces';
import { NotificationsSlot } from '../skeleton/slot-wrappers';

import sharedStyles from '../../resize/styles.css.js';
import testutilStyles from '../../test-classes/styles.css.js';
Expand Down Expand Up @@ -80,4 +81,7 @@ export function AppLayoutNavigationImplementation({ appLayoutInternals }: AppLay
);
}

export const createWidgetizedAppLayoutNavigation = createWidgetizedComponent(AppLayoutNavigationImplementation);
export const createWidgetizedAppLayoutNavigation = createWidgetizedComponent(
AppLayoutNavigationImplementation,
NotificationsSlot
);
8 changes: 6 additions & 2 deletions src/app-layout/visual-refresh-toolbar/notifications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal
import { highContrastHeaderClassName } from '../../../internal/utils/content-header-utils';
import { createWidgetizedComponent } from '../../../internal/widgets';
import { AppLayoutInternals } from '../interfaces';
import { NotificationsSkeleton } from '../skeleton/slot-skeletons';
import { NotificationsSlot } from '../skeleton/slot-wrappers';

import testutilStyles from '../../test-classes/styles.css.js';
import styles from './styles.css.js';

interface AppLayoutNotificationsImplementationProps {
export interface AppLayoutNotificationsImplementationProps {
appLayoutInternals: AppLayoutInternals;
children: React.ReactNode;
}
Expand Down Expand Up @@ -59,4 +60,7 @@ export function AppLayoutNotificationsImplementation({
);
}

export const createWidgetizedAppLayoutNotifications = createWidgetizedComponent(AppLayoutNotificationsImplementation);
export const createWidgetizedAppLayoutNotifications = createWidgetizedComponent(
AppLayoutNotificationsImplementation,
NotificationsSkeleton
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { BreadcrumbGroupImplementation } from '../../../../breadcrumb-group/implementation';
import { BreadcrumbGroupProps } from '../../../../breadcrumb-group/interfaces';
import { BreadcrumbsSlotContext } from '../../contexts';

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

interface BreadcrumbsSlotProps {
ownBreadcrumbs: React.ReactNode;
discoveredBreadcrumbs: BreadcrumbGroupProps | null;
}

export function BreadcrumbsSlot({ ownBreadcrumbs, discoveredBreadcrumbs }: BreadcrumbsSlotProps) {
return (
<BreadcrumbsSlotContext.Provider value={{ isInToolbar: true }}>
<div className={styles['breadcrumbs-own']}>{ownBreadcrumbs}</div>
{discoveredBreadcrumbs && (
<div className={styles['breadcrumbs-discovered']}>
<BreadcrumbGroupImplementation
{...discoveredBreadcrumbs}
data-awsui-discovered-breadcrumbs={true}
__injectAnalyticsComponentMetadata={true}
/>
</div>
)}
</BreadcrumbsSlotContext.Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

// backward compatibility before this commit: 7a4b7b3e3b1d50830383805a8f4ab6cd93c9701f
.breadcrumbs-own:not(:empty) + .breadcrumbs-discovered {
display: none;
}
23 changes: 23 additions & 0 deletions src/app-layout/visual-refresh-toolbar/skeleton/slot-skeletons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { AppLayoutNotificationsImplementationProps } from '../notifications';
import { AppLayoutToolbarImplementationProps } from '../toolbar';
import { BreadcrumbsSlot } from './breadcrumbs';
import { NotificationsSlot, ToolbarSlot } from './slot-wrappers';

export const ToolbarSkeleton = React.forwardRef<HTMLElement, AppLayoutToolbarImplementationProps>(
({ appLayoutInternals }: AppLayoutToolbarImplementationProps, ref) => (
<ToolbarSlot ref={ref}>
<BreadcrumbsSlot
ownBreadcrumbs={appLayoutInternals.breadcrumbs}
discoveredBreadcrumbs={appLayoutInternals.discoveredBreadcrumbs}
/>
</ToolbarSlot>
)
);

export const NotificationsSkeleton = React.forwardRef<HTMLElement, AppLayoutNotificationsImplementationProps>(
(props: AppLayoutNotificationsImplementationProps, ref) => <NotificationsSlot ref={ref} />
);
27 changes: 11 additions & 16 deletions src/app-layout/visual-refresh-toolbar/toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import clsx from 'clsx';

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

import { BreadcrumbGroupImplementation } from '../../../breadcrumb-group/implementation';
import { createWidgetizedComponent } from '../../../internal/widgets';
import { AppLayoutProps } from '../../interfaces';
import { Focusable, FocusControlMultipleStates } from '../../utils/use-focus-control';
import { BreadcrumbsSlotContext } from '../contexts';
import { AppLayoutInternals } from '../interfaces';
import { BreadcrumbsSlot } from '../skeleton/breadcrumbs';
import { ToolbarSkeleton } from '../skeleton/slot-skeletons';
import { ToolbarSlot } from '../skeleton/slot-wrappers';
import { DrawerTriggers, SplitPanelToggleProps } from './drawer-triggers';
import TriggerButton from './trigger-button';
Expand Down Expand Up @@ -48,7 +48,7 @@ export interface ToolbarProps {
onActiveGlobalDrawersChange?: ((drawerId: string) => void) | undefined;
}

interface AppLayoutToolbarImplementationProps {
export interface AppLayoutToolbarImplementationProps {
appLayoutInternals: AppLayoutInternals;
toolbarProps: ToolbarProps;
}
Expand Down Expand Up @@ -194,18 +194,10 @@ export function AppLayoutToolbarImplementation({
)}
{(breadcrumbs || discoveredBreadcrumbs) && (
<div className={clsx(styles['universal-toolbar-breadcrumbs'], testutilStyles.breadcrumbs)}>
<BreadcrumbsSlotContext.Provider value={{ isInToolbar: true }}>
<div className={styles['breadcrumbs-own']}>{breadcrumbs}</div>
{discoveredBreadcrumbs && (
<div className={styles['breadcrumbs-discovered']}>
<BreadcrumbGroupImplementation
{...discoveredBreadcrumbs}
data-awsui-discovered-breadcrumbs={true}
__injectAnalyticsComponentMetadata={true}
/>
</div>
)}
</BreadcrumbsSlotContext.Provider>
<BreadcrumbsSlot
ownBreadcrumbs={appLayoutInternals.breadcrumbs}
discoveredBreadcrumbs={appLayoutInternals.discoveredBreadcrumbs}
/>
</div>
)}
{((drawers && drawers.length > 0) || (hasSplitPanel && splitPanelToggleProps?.displayed)) && (
Expand All @@ -232,4 +224,7 @@ export function AppLayoutToolbarImplementation({
);
}

export const createWidgetizedAppLayoutToolbar = createWidgetizedComponent(AppLayoutToolbarImplementation);
export const createWidgetizedAppLayoutToolbar = createWidgetizedComponent(
AppLayoutToolbarImplementation,
ToolbarSkeleton
);
5 changes: 0 additions & 5 deletions src/app-layout/visual-refresh-toolbar/toolbar/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,6 @@
}
}

// backward compatibility before this commit: 7a4b7b3e3b1d50830383805a8f4ab6cd93c9701f
.breadcrumbs-own:not(:empty) + .breadcrumbs-discovered {
display: none;
}

.block-body-scroll {
overflow: hidden;
}
48 changes: 47 additions & 1 deletion src/breadcrumb-group/__tests__/breadcrumb-group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { act, render } from '@testing-library/react';

import BreadcrumbGroup, { BreadcrumbGroupProps } from '../../../lib/components/breadcrumb-group';
import TestI18nProvider from '../../../lib/components/i18n/testing';
import createWrapper, { BreadcrumbGroupWrapper } from '../../../lib/components/test-utils/dom';
import { DATA_ATTR_RESOURCE_TYPE, getFunnelNameSelector } from '../../../lib/components/internal/analytics/selectors';
import createWrapper, { BreadcrumbGroupWrapper, ElementWrapper } from '../../../lib/components/test-utils/dom';

import itemStyles from '../../../lib/components/breadcrumb-group/item/styles.css.js';
import styles from '../../../lib/components/breadcrumb-group/styles.css.js';
Expand Down Expand Up @@ -231,4 +232,49 @@ describe('BreadcrumbGroup Component', () => {
});
});
});

describe('funnel attributes', () => {
function getElementsText(elements: Array<ElementWrapper>) {
return Array.from(elements).map(element => element.getElement().textContent);
}

function getFunnelNameElements(wrapper: BreadcrumbGroupWrapper) {
return wrapper.findAll(getFunnelNameSelector());
}

function getResourceTypeElements(wrapper: BreadcrumbGroupWrapper) {
return wrapper.findAll(`[${DATA_ATTR_RESOURCE_TYPE}]`);
}

test('should add funnel name and resource type attributes', () => {
const wrapper = renderBreadcrumbGroup({
items: [
{ text: 'Home', href: '/home' },
{ text: 'Resource', href: '/resource' },
{ text: 'Name', href: '/resource/name' },
],
});
expect(getElementsText(getResourceTypeElements(wrapper))).toEqual(['Resource']);
expect(getElementsText(getFunnelNameElements(wrapper))).toEqual(['Name']);
});

test('allows funnel name and resource type to be the same item', () => {
const wrapper = renderBreadcrumbGroup({
items: [
{ text: 'Home', href: '/home' },
{ text: 'Page', href: '/page' },
],
});
expect(getElementsText(getResourceTypeElements(wrapper))).toEqual(['Page']);
expect(getElementsText(getFunnelNameElements(wrapper))).toEqual(['Page']);
});

test('only adds funnel name if there is only one item', () => {
const wrapper = renderBreadcrumbGroup({
items: [{ text: 'Home', href: '/home' }],
});
expect(getElementsText(getResourceTypeElements(wrapper))).toEqual([]);
expect(getElementsText(getFunnelNameElements(wrapper))).toEqual(['Home']);
});
});
});
14 changes: 6 additions & 8 deletions src/breadcrumb-group/__tests__/breadcrumb-item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom';

import BreadcrumbGroup, { BreadcrumbGroupProps } from '../../../lib/components/breadcrumb-group';
import { BreadcrumbItem } from '../../../lib/components/breadcrumb-group/item/item';
import { DATA_ATTR_FUNNEL_KEY, FUNNEL_KEY_FUNNEL_NAME } from '../../../lib/components/internal/analytics/selectors';
import createWrapper, { BreadcrumbGroupWrapper } from '../../../lib/components/test-utils/dom';

import breadcrumbItemStyles from '../../../lib/components/breadcrumb-group/item/styles.selectors.js';
Expand Down Expand Up @@ -128,17 +127,16 @@ describe('BreadcrumbGroup Item', () => {
lastLink.click();
expect(onFollowSpy).not.toHaveBeenCalled();
});

test('should add a data-analytics attribute for the funnel name to the last item', () => {
const expectedFunnelName = items[items.length - 1].text;
const element = wrapper.find(`[${DATA_ATTR_FUNNEL_KEY}="${FUNNEL_KEY_FUNNEL_NAME}"]`)!.getElement();
expect(element.innerHTML).toBe(expectedFunnelName);
});
});

test('displays tooltip', () => {
const { container } = render(
<BreadcrumbItem item={{ text: 'Long Breadcrumb text', href: '#' }} isTruncated={true} />
<BreadcrumbItem
itemIndex={0}
totalCount={1}
item={{ text: 'Long Breadcrumb text', href: '#' }}
isTruncated={true}
/>
);
const elementAnchor = createWrapper(container).find(`.${breadcrumbItemStyles.anchor}`)!.getElement();
elementAnchor.focus();
Expand Down
Loading

0 comments on commit c5a8373

Please sign in to comment.