Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Drawer badge, mobile and overflow #1416

Merged
merged 21 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 73 additions & 18 deletions pages/app-layout/with-drawers.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ import AppContext, { AppContextType } from '../app/app-context';

type DemoContext = React.Context<AppContextType<{ hasTools: boolean | undefined; hasDrawers: boolean | undefined }>>;

const getAriaLabels = (title: string, badge: boolean) => {
return {
closeButton: `${title} close button`,
content: `${title}`,
triggerButton: `${title} trigger button${badge ? ' (Unread notifications)' : ''}`,
resizeHandle: `${title} resize handle`,
};
};

export default function WithDrawers() {
const { urlParams, setUrlParams } = useContext(AppContext as DemoContext);
const [activeDrawerId, setActiveDrawerId] = useState<string | null>(null);
Expand All @@ -30,15 +39,11 @@ export default function WithDrawers() {
: {
drawers: {
ariaLabel: 'Drawers',
overflowAriaLabel: 'Overflow drawers',
activeDrawerId: activeDrawerId,
items: [
{
ariaLabels: {
closeButton: 'Security close button',
content: 'Security drawer content',
triggerButton: 'Security trigger button',
resizeHandle: 'Security resize handle',
},
ariaLabels: getAriaLabels('Security', false),
content: <Security />,
id: 'security',
resizable: true,
Expand All @@ -53,25 +58,17 @@ export default function WithDrawers() {
},
},
{
ariaLabels: {
closeButton: 'ProHelp close button',
content: 'ProHelp drawer content',
triggerButton: 'ProHelp trigger button',
resizeHandle: 'ProHelp resize handle',
},
ariaLabels: getAriaLabels('Pro help', true),
content: <ProHelp />,
badge: true,
defaultSize: 600,
id: 'pro-help',
trigger: {
iconName: 'contact',
},
},
{
ariaLabels: {
closeButton: 'Links close button',
content: 'Links drawer content',
triggerButton: 'Links trigger button',
resizeHandle: 'Links resize handle',
},
ariaLabels: getAriaLabels('Links', false),
resizable: true,
defaultSize: 500,
content: <Links />,
Expand All @@ -80,6 +77,64 @@ export default function WithDrawers() {
iconName: 'share',
},
},
{
ariaLabels: getAriaLabels('Test 1', true),
content: <HelpPanel header={<h2>Test 1</h2>}>Test 1.</HelpPanel>,
badge: true,
id: 'test-1',
trigger: {
iconName: 'contact',
},
},
{
ariaLabels: getAriaLabels('Test 2', false),
resizable: true,
defaultSize: 500,
content: <HelpPanel header={<h2>Test 2</h2>}>Test 2.</HelpPanel>,
id: 'test-2',
trigger: {
iconName: 'share',
},
},
{
ariaLabels: getAriaLabels('Test 3', true),
content: <HelpPanel header={<h2>Test 3</h2>}>Test 3.</HelpPanel>,
badge: true,
id: 'test-3',
trigger: {
iconName: 'contact',
},
},
{
ariaLabels: getAriaLabels('Test 4', false),
resizable: true,
defaultSize: 500,
content: <HelpPanel header={<h2>Test 4</h2>}>Test 4.</HelpPanel>,
id: 'test-4',
trigger: {
iconName: 'edit',
},
},
{
ariaLabels: getAriaLabels('Test 5', false),
resizable: true,
defaultSize: 500,
content: <HelpPanel header={<h2>Test 5</h2>}>Test 5.</HelpPanel>,
id: 'test-5',
trigger: {
iconName: 'add-plus',
},
},
{
ariaLabels: getAriaLabels('Test 6', false),
resizable: true,
defaultSize: 500,
content: <HelpPanel header={<h2>Test 6</h2>}>Test 6.</HelpPanel>,
id: 'test-6',
trigger: {
iconName: 'call',
},
},
] as DrawerItem[],
onChange: (event: NonCancelableCustomEvent<string>) => {
setActiveDrawerId(event.detail);
Expand Down
2 changes: 1 addition & 1 deletion src/app-layout/__integ__/app-layout-drawers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class AppLayoutDrawersPage extends BasePageObject {
}

async openThirdPanel() {
await this.click(wrapper.findDrawersTriggers().get(3).toSelector());
await this.click(wrapper.findDrawerTriggerById('links').toSelector());
}

async dragResizerTo({ x: targetX, y: targetY }: { x: number; y: number }) {
Expand Down
5 changes: 5 additions & 0 deletions src/app-layout/__tests__/common.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import {
} from './utils';
import AppLayout from '../../../lib/components/app-layout';

jest.mock('@cloudscape-design/component-toolkit', () => ({
...jest.requireActual('@cloudscape-design/component-toolkit'),
useContainerQuery: () => [100, () => {}],
}));

describeEachAppLayout(() => {
test('Default state', () => {
const { wrapper } = renderComponent(<AppLayout />);
Expand Down
20 changes: 20 additions & 0 deletions src/app-layout/__tests__/desktop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import {
resizableDrawer,
singleDrawer,
singleDrawerOpen,
manyDrawers,
} from './utils';
import AppLayout, { AppLayoutProps } from '../../../lib/components/app-layout';
import styles from '../../../lib/components/app-layout/styles.css.js';
import notificationStyles from '../../../lib/components/app-layout/notifications/styles.css.js';
import visualRefreshStyles from '../../../lib/components/app-layout/visual-refresh/styles.css.js';
import iconStyles from '../../../lib/components/icon/styles.css.js';
import customCssProps from '../../../lib/components/internal/generated/custom-css-properties';
import { KeyCode } from '../../internal/keycode';
import { useVisualRefresh } from '../../../lib/components/internal/hooks/use-visual-mode';
Expand Down Expand Up @@ -232,6 +234,12 @@ describeEachThemeAppLayout(false, () => {
act(() => wrapper.findDrawersTriggers()![0].click());
expect(wrapper.findDrawersSlider()!.getElement()).toHaveAttribute('aria-valuenow', '0');
});

test('should render overflow item when expected', () => {
const { wrapper } = renderComponent(<AppLayout contentType="form" {...manyDrawers} />);

expect(wrapper.findDrawersTriggers()!.length).toBeLessThan(100);
});
});

// In VR we use a custom CSS property so we cannot test the style declaration.
Expand Down Expand Up @@ -272,6 +280,12 @@ describe('Classic only features', () => {
act(() => wrapper.findDrawersTriggers()![0].click());
expect(wrapper.findActiveDrawer()!.getElement().style.width).toBe('500px');
});

test('should render badge when defined', () => {
const { wrapper } = renderComponent(<AppLayout contentType="form" {...manyDrawers} />);

expect(wrapper.findByClassName(iconStyles.badge)!.getElement()).toBeInTheDocument();
});
});

describe('VR only features', () => {
Expand All @@ -287,4 +301,10 @@ describe('VR only features', () => {
act(() => wrapper.findDrawersTriggers()![0].click());
expect(wrapper.findActiveDrawer()!.getElement()).toHaveClass(styles['with-motion']);
});

test('should render badge when defined', () => {
const { wrapper } = renderComponent(<AppLayout contentType="form" {...manyDrawers} />);

expect(wrapper.findByClassName(visualRefreshStyles.badge)!.getElement()).toBeInTheDocument();
});
});
21 changes: 20 additions & 1 deletion src/app-layout/__tests__/drawers.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { describeEachAppLayout, renderComponent, singleDrawer } 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';

jest.mock('../../../lib/components/internal/hooks/use-mobile', () => ({
useMobile: jest.fn().mockReturnValue(true),
}));

jest.mock('@cloudscape-design/component-toolkit', () => ({
...jest.requireActual('@cloudscape-design/component-toolkit'),
useContainerQuery: () => [100, () => {}],
}));

describeEachAppLayout(() => {
test(`should not render drawer when it is not defined`, () => {
const { wrapper, rerender } = renderComponent(<AppLayout contentType="form" {...singleDrawer} />);
Expand All @@ -33,4 +41,15 @@ describeEachAppLayout(() => {

expect(wrapper.findDrawersTriggers()).toHaveLength(2);
});

test('should open active drawer on click of overflow item', () => {
const { container } = render(<AppLayout contentType="form" {...manyDrawers} />);
const wrapper = createWrapper(container).findAppLayout()!;
const buttonDropdown = createWrapper(container).findButtonDropdown();

expect(wrapper.findActiveDrawer()).toBeFalsy();
buttonDropdown!.openDropdown();
buttonDropdown!.findItemById('5')!.click();
expect(wrapper.findActiveDrawer()).toBeTruthy();
});
});
8 changes: 8 additions & 0 deletions src/app-layout/__tests__/mobile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import {
renderComponent,
singleDrawer,
singleDrawerOpen,
manyDrawers,
splitPanelI18nStrings,
} from './utils';
import AppLayout, { AppLayoutProps } from '../../../lib/components/app-layout';
import SplitPanel from '../../../lib/components/split-panel';
import { AppLayoutWrapper } from '../../../lib/components/test-utils/dom';
import styles from '../../../lib/components/app-layout/styles.css.js';
import toolbarStyles from '../../../lib/components/app-layout/mobile-toolbar/styles.css.js';
import iconStyles from '../../../lib/components/icon/styles.css.js';
import testUtilsStyles from '../../../lib/components/app-layout/test-classes/styles.css.js';

import visualRefreshRefactoredStyles from '../../../lib/components/app-layout/visual-refresh/styles.css.js';
import { findUpUntil } from '../../../lib/components/internal/utils/dom';

Expand Down Expand Up @@ -354,4 +357,9 @@ describeEachThemeAppLayout(true, theme => {
expect(wrapper.findDrawersTriggers()![0].getElement()).toHaveAttribute('aria-label', 'Security trigger button');
expect(wrapper.findDrawersMobileTriggersContainer()!.getElement()).toHaveAttribute('aria-label', 'Drawers');
});

test('should render badge when defined', () => {
const { wrapper } = renderComponent(<AppLayout contentType="form" {...manyDrawers} />);
expect(wrapper.findDrawersTriggers()[0]!.getElement().children[0]).toHaveClass(iconStyles.badge);
katiegeorge marked this conversation as resolved.
Show resolved Hide resolved
});
});
5 changes: 5 additions & 0 deletions src/app-layout/__tests__/runtime-drawers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ beforeEach(() => {
awsuiPluginsInternal.appLayout.clearRegisteredDrawers();
});

jest.mock('@cloudscape-design/component-toolkit', () => ({
...jest.requireActual('@cloudscape-design/component-toolkit'),
useContainerQuery: () => [1300, () => {}],
}));

async function renderComponent(jsx: React.ReactElement) {
const { container, rerender } = render(jsx);
const wrapper = createWrapper(container).findAppLayout()!;
Expand Down
42 changes: 42 additions & 0 deletions src/app-layout/__tests__/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import styles from '../../../lib/components/app-layout/styles.css.js';
import visualRefreshStyles from '../../../lib/components/app-layout/visual-refresh/styles.css.js';
import testutilStyles from '../../../lib/components/app-layout/test-classes/styles.css.js';
import { InternalDrawerProps, DrawerItem } from '../../../lib/components/app-layout/drawer/interfaces';
import { IconProps } from '../../../lib/components/icon/interfaces';

// Mock element queries result. Note that in order to work, this mock should be applied first, before the AppLayout is required
jest.mock('../../../lib/components/internal/hooks/use-mobile', () => ({
Expand Down Expand Up @@ -133,6 +134,47 @@ export const singleDrawer: Required<InternalDrawerProps> = {
},
};

const getDrawerItem = (id: string, iconName: IconProps.Name) => {
return {
ariaLabels: {
closeButton: `${id} close button`,
content: `${id} drawer content`,
triggerButton: `${id} trigger button`,
resizeHandle: `${id} resize handle`,
},
content: <span>{id}</span>,
id,
trigger: {
iconName,
},
};
};

const manyDrawersArray = [...Array(100).keys()].map(item => item.toString());

export const manyDrawers: Required<InternalDrawerProps> = {
drawers: {
ariaLabel: 'Drawers',
items: [
{
ariaLabels: {
closeButton: 'Security close button',
content: 'Security drawer content',
triggerButton: 'Security trigger button',
resizeHandle: 'Security resize handle',
},
content: <span>Security</span>,
badge: true,
id: 'security',
trigger: {
iconName: 'security',
},
},
...manyDrawersArray.map(item => getDrawerItem(item, 'security')),
],
},
};

export const singleDrawerOpen: Required<InternalDrawerProps> = {
drawers: {
ariaLabel: 'Drawers',
Expand Down
50 changes: 50 additions & 0 deletions src/app-layout/drawer/__tests__/drawers-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { splitItems } from '../../../../lib/components/app-layout/drawer/drawers-helpers';

test('handles empty values', () => {
expect(splitItems(undefined, 2, undefined)).toEqual({ visibleItems: [], overflowItems: [] });
});

test('splits items by index', () => {
expect(splitItems([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], 2, undefined)).toEqual({
visibleItems: [{ id: '1' }, { id: '2' }],
overflowItems: [{ id: '3' }, { id: '4' }],
});
});

test('splits items when active item is visible', () => {
expect(splitItems([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], 2, '2')).toEqual({
visibleItems: [{ id: '1' }, { id: '2' }],
overflowItems: [{ id: '3' }, { id: '4' }],
});
});

test('moves active item to visible when in overflow', () => {
expect(splitItems([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], 2, '3')).toEqual({
visibleItems: [{ id: '1' }, { id: '3' }],
overflowItems: [{ id: '2' }, { id: '4' }],
});
});

test('does not fail when active id resolves to a non-existing item', () => {
expect(splitItems([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], 2, '10')).toEqual({
visibleItems: [{ id: '1' }, { id: '2' }],
overflowItems: [{ id: '3' }, { id: '4' }],
});
});

test('moves single overflow item into visible items', () => {
expect(splitItems([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], 3, undefined)).toEqual({
visibleItems: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }],
overflowItems: [],
});
});

test('when mobile and items length is 3, adds one to split index', () => {
expect(splitItems([{ id: '1' }, { id: '2' }, { id: '3' }], 2, undefined, true)).toEqual({
visibleItems: [{ id: '1' }, { id: '2' }, { id: '3' }],
overflowItems: [],
});
});
Loading
Loading