Skip to content

Commit

Permalink
feat: Make breadcrumb responsive
Browse files Browse the repository at this point in the history
  • Loading branch information
Francesco Longo committed Sep 2, 2024
1 parent 68647dc commit 93a32ff
Show file tree
Hide file tree
Showing 17 changed files with 525 additions and 148 deletions.
9 changes: 8 additions & 1 deletion pages/breadcrumb-group/events.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SpaceBetween } from '~components';
import BreadcrumbGroup, { BreadcrumbGroupProps } from '~components/breadcrumb-group';

import ScreenshotArea from '../utils/screenshot-area';
const items = [
const defaultItems = [
'First that is very very very very very very long long long text',
'Second',
'Third',
Expand All @@ -27,6 +27,7 @@ export default function ButtonDropdownPage() {
const onClickCallback = (event: CustomEvent<BreadcrumbGroupProps.ClickDetail>) => {
setOnClickMessage(`OnClick: ${event.detail.text} item was selected`);
};
const [items, setItems] = useState(defaultItems);
return (
<ScreenshotArea disableAnimations={true}>
<article>
Expand Down Expand Up @@ -56,6 +57,12 @@ export default function ButtonDropdownPage() {
items={shortItems.map(text => ({ text, href: `#` }))}
/>
</div>
<button type="button" id="add" onClick={() => setItems([...items, defaultItems[5]])}>
Add
</button>
<button type="button" id="remove" onClick={() => setItems(items.slice(0, items.length - 1))}>
Remove
</button>
</SpaceBetween>
</article>
</ScreenshotArea>
Expand Down
63 changes: 63 additions & 0 deletions pages/breadcrumb-group/responsive.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import Box from '~components/box';
import BreadcrumbGroup from '~components/breadcrumb-group';
import SpaceBetween from '~components/space-between';

export default function ResponsiveBreadcrumbsPage() {
return (
<article>
<Box padding="xl">
<SpaceBetween size="xxl">
<h1>Responsive breadcrumbs</h1>
<ResponsiveBreadcrumbs
widths={[900, 800, 700, 600, 500, 400, 300, 200]}
items={[
'A',
'Longer breadrcumb',
'ABC',
'Another even longer breadcrumb',
'ABCDEF',
'ABCDEFGHIJsjbdkasbdhjabsjdhasjhdabsjd',
]}
/>
<ResponsiveBreadcrumbs widths={[150]} items={['Small', 'Small', 'Small', 'Small', 'Small']} />
<ResponsiveBreadcrumbs
widths={[150]}
items={['Large breadcrumb', 'Large breadcrumb', 'Large breadcrumb', 'Large breadcrumb', 'Large breadcrumb']}
/>
<ResponsiveBreadcrumbs widths={[100]} items={['Small', 'Small']} />
<ResponsiveBreadcrumbs widths={[100]} items={['Large breadcrumb', 'Large breadcrumb']} />
<ResponsiveBreadcrumbs widths={[100]} items={['Large breadcrumb', 'Small']} />
<ResponsiveBreadcrumbs widths={[30]} items={['Small']} />
<ResponsiveBreadcrumbs widths={[30]} items={['Large breadcrumb']} />
</SpaceBetween>
</Box>
</article>
);
}

interface ResponsiveBreadcrumbsProps {
items: Array<string>;
widths: Array<number>;
}

const ResponsiveBreadcrumbs = ({ items, widths }: ResponsiveBreadcrumbsProps) => {
const breadcrumbs = items.map(text => ({ text, href: `#` }));
return (
<SpaceBetween size="xxl">
{widths.map((width, key) => (
<div key={key} style={{ width, borderInlineEnd: '2px solid blue' }}>
<BreadcrumbGroup
key={key}
ariaLabel="Navigation long text"
expandAriaLabel="Show path for long text"
items={breadcrumbs}
/>
</div>
))}
</SpaceBetween>
);
};
60 changes: 60 additions & 0 deletions pages/breadcrumb-group/responsiveness.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';

import { Button, FormField, SpaceBetween, Textarea } from '~components';
import BreadcrumbGroup from '~components/breadcrumb-group';
import Input from '~components/input';

export default function ResponsiveBreadcrumbsPage() {
const [itemsLists, setItemsLists] = useState([
['A', 'AB', 'ABC', 'ABCD', 'ABCDEF', 'ABCDEFGHIJsjbdkasbdhjabsjdhasjhdabsjd'],
['EC2', 'Instances', 'i-03abdc1839101a53f'],
[
'Amazon S3',
'Buckets',
'164981592106-us-east-1-athena-results-bucket-n8qxkwo99y',
'5d04ca725b8547220d019af4d3g0ae11',
],
]);
const [minBreadcrumbWidth, setMinBreadcrumbWidth] = useState<string | undefined>('120');
const [newBreadcrumb, setNewBreadcrumb] = useState<string>('["A", "AB", "ABCDEFGHIJsjbdkasbdhjabsjdhasjhdabsjd"]');
return (
<article>
<SpaceBetween size="xxl">
<h1>Responsive breadcrumbs</h1>
<FormField label="Enter minimum width">
<Input
type="number"
value={minBreadcrumbWidth || ''}
onChange={evt => {
setMinBreadcrumbWidth(evt.detail.value || undefined);
}}
/>
</FormField>
<FormField
label="Add breadcrumb"
secondaryControl={
<Button onClick={() => setItemsLists([...itemsLists, JSON.parse(newBreadcrumb)])}>Add</Button>
}
>
<Textarea
value={newBreadcrumb}
onChange={evt => {
setNewBreadcrumb(evt.detail.value);
}}
/>
</FormField>
<hr />
{itemsLists.map((items, key) => (
<BreadcrumbGroup
key={key}
ariaLabel="Navigation long text"
expandAriaLabel="Show path for long text"
items={items.map(text => ({ text, href: `#` }))}
/>
))}
</SpaceBetween>
</article>
);
}
6 changes: 4 additions & 2 deletions src/app-layout/__tests__/global-breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,9 @@ describe('without feature flag', () => {

test('renders analytics metadata information', async () => {
activateAnalyticsMetadata(true);
await renderAsync(<AppLayout content={<BreadcrumbGroup items={defaultBreadcrumbs} />} />);
await renderAsync(
<AppLayout content={<BreadcrumbGroup items={defaultBreadcrumbs} ariaLabel="global breadcrumbs" />} />
);
const breadcrumbsWrapper = wrapper.findAppLayout()!.findContentRegion().findBreadcrumbGroup()!;
const firstBreadcrumb = breadcrumbsWrapper.findBreadcrumbLink(1)!.getElement();
expect(getGeneratedAnalyticsMetadata(firstBreadcrumb)).toEqual({
Expand All @@ -278,7 +280,7 @@ test('renders analytics metadata information', async () => {
type: 'component',
detail: {
name: 'awsui.BreadcrumbGroup',
label: 'Home...Page',
label: 'global breadcrumbs',
},
},
],
Expand Down
56 changes: 53 additions & 3 deletions src/breadcrumb-group/__integ__/breadcrumb-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';

import createWrapper from '../../../lib/components/test-utils/selectors';

import breadcrumbGroupStyles from '../../../lib/components/breadcrumb-group/styles.selectors.js';
import tooltipStyles from '../../../lib/components/internal/components/tooltip/styles.selectors.js';

const breadcrumbGroupWrapper = createWrapper().findBreadcrumbGroup();
Expand All @@ -15,7 +16,7 @@ const dropdownItemsSelector = dropdownWrapper.findItems().toSelector();

class BreadcrumbGroupPage extends BasePageObject {
setMobileViewport() {
return this.setWindowSize({ width: 600, height: 800 });
return this.setWindowSize({ width: 400, height: 800 });
}
isDropdownVisible() {
return this.isDisplayed(dropdownSelector);
Expand All @@ -32,6 +33,14 @@ class BreadcrumbGroupPage extends BasePageObject {
clickItem(index: number) {
return this.click(dropdownWrapper.findItems().get(index).toSelector());
}
getActiveElemenId() {
return this.browser.execute(function () {
return document.activeElement!.id;
});
}
isEllipsisVisible() {
return this.isExisting(`.${breadcrumbGroupStyles.ellipsis}.${breadcrumbGroupStyles.visible}`);
}
}
const setupTest = (testFn: (page: BreadcrumbGroupPage, browser: WebdriverIO.Browser) => Promise<void>) => {
return useBrowser(async browser => {
Expand All @@ -45,11 +54,37 @@ describe('BreadcrumbGroup', () => {
test(
'Has proper number of items in the dropdown',
setupTest(async page => {
await page.setMobileViewport();
await page.setWindowSize({ width: 645, height: 800 });
await page.openDropdown();
expect(await page.getElementsCount(dropdownItemsSelector)).toBe(1);
await page.closeDropdown();

await page.setWindowSize({ width: 570, height: 800 });
await page.openDropdown();
expect(await page.getElementsCount(dropdownItemsSelector)).toBe(2);
await page.closeDropdown();

await page.setWindowSize({ width: 500, height: 800 });
await page.openDropdown();
expect(await page.getElementsCount(dropdownItemsSelector)).toBe(3);
await page.closeDropdown();

await page.setWindowSize({ width: 400, height: 800 });
await page.openDropdown();
expect(await page.getElementsCount(dropdownItemsSelector)).toBe(4);
})
);
test(
'Adjusts display when adding/removing items',
setupTest(async page => {
await page.setWindowSize({ width: 700, height: 800 });
expect(page.isEllipsisVisible()).resolves.toBe(false);
await page.click('#add');
expect(page.isEllipsisVisible()).resolves.toBe(true);
await page.click('#remove');
expect(page.isEllipsisVisible()).resolves.toBe(false);
})
);

test(
'Dropdown trigger [...] is focused after the dropdown is closed',
Expand Down Expand Up @@ -103,11 +138,26 @@ describe('BreadcrumbGroup', () => {
);

test(
'Attachs funnel name attribute to last breadcrumb item',
'Attaches funnel name attribute to last breadcrumb item',
setupTest(async (page, browser) => {
await page.setMobileViewport();
const funnelName = await browser.$('[data-analytics-funnel-key="funnel-name"]').getText();
expect(funnelName).toBe('Sixth that is very very very very very very long long long text');
})
);

test(
'Focus does not go into ghost replica',
setupTest(async page => {
await page.setWindowSize({ width: 1200, height: 800 });
await page.click('#focus-target-long-text');
await page.keys('Tab');
await page.keys('Tab');
await page.keys('Tab');
await page.keys('Tab');
await page.keys('Tab');
await page.keys('Tab');
await expect(page.getActiveElemenId()).resolves.toBe('focus-target-short-text');
})
);
});
16 changes: 12 additions & 4 deletions src/breadcrumb-group/__tests__/analytics-metadata.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import { activateAnalyticsMetadata } from '@cloudscape-design/component-toolkit/
import { getGeneratedAnalyticsMetadata } from '@cloudscape-design/component-toolkit/internal/analytics-metadata/utils';

import BreadcrumbGroup from '../../../lib/components/breadcrumb-group';
import { useMobile } from '../../../lib/components/internal/hooks/use-mobile';
import { getItemsDisplayProperties } from '../../../lib/components/breadcrumb-group/utils';
import createWrapper from '../../../lib/components/test-utils/dom';
import { validateComponentNameAndLabels } from '../../internal/__tests__/analytics-metadata-test-utils';

import ownLabels from '../../../lib/components/breadcrumb-group/analytics-metadata/styles.css.js';
import buttonDropdownLabels from '../../../lib/components/button-dropdown/analytics-metadata/styles.css.js';

jest.mock('../../../lib/components/internal/hooks/use-mobile', () => ({
useMobile: jest.fn().mockReturnValue(false),
jest.mock('../../../lib/components/breadcrumb-group/utils', () => ({
getItemsDisplayProperties: jest.fn().mockReturnValue({
shrinkFactors: [0, 0, 0, 0],
minWidths: [100, 100, 100, 100],
collapsed: 0,
}),
}));

const labels = { ...ownLabels, ...buttonDropdownLabels };
Expand Down Expand Up @@ -79,7 +83,11 @@ describe('BreadcrumbGroup renders correct analytics metadata', () => {
});
});
test('in mobile view', () => {
(useMobile as jest.Mock).mockReturnValue(true);
(getItemsDisplayProperties as jest.Mock).mockReturnValue({
shrinkFactors: [0, 0, 0, 0],
minWidths: [100, 100, 100, 100],
collapsed: 2,
});
const wrapper = renderBreadcrumbGroup();

const firstBreadcrumb = wrapper.findBreadcrumbLink(1)!.getElement();
Expand Down
39 changes: 36 additions & 3 deletions src/breadcrumb-group/__tests__/breadcrumb-group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ describe('BreadcrumbGroup Component', () => {
// Test for AWSUI-6738
test('all the icons stay visible when changing the items', () => {
const { container, rerender } = render(<BreadcrumbGroup items={items} />);
const wrapper = createWrapper(container).findBreadcrumbGroup(`.${styles['breadcrumb-group']}`)!;
const getIcons = () => wrapper.findAll(`.${itemStyles.icon}`);
const wrapper = createWrapper(container).findBreadcrumbGroup()!;
const getIcons = () => wrapper.findAll(`.${itemStyles.breadcrumb} .${itemStyles.icon}`);
expect(getIcons()).toHaveLength(2);
rerender(<BreadcrumbGroup items={items.slice(0, 2)} />);
rerender(<BreadcrumbGroup items={[]} />);
Expand Down Expand Up @@ -141,7 +141,7 @@ describe('BreadcrumbGroup Component', () => {
}}
/>
);
const wrapper = createWrapper(container).findBreadcrumbGroup(`.${styles['breadcrumb-group']}`)!;
const wrapper = createWrapper(container).findBreadcrumbGroup()!;
wrapper.findBreadcrumbLink(2)!.click();
expect(onClick).toHaveBeenCalledWith(items[1]);
});
Expand Down Expand Up @@ -198,4 +198,37 @@ describe('BreadcrumbGroup Component', () => {
expect(wrapper.findDropdown()?.findNativeButton().getElement()).toHaveAttribute('aria-label', 'Custom show path');
});
});

describe('Ghost breadcrumb group', () => {
test('has aria-hidden property', () => {
const breadCrumbGroup = renderBreadcrumbGroup({ items: [] });
expect(breadCrumbGroup.find(`.${styles.ghost}`)?.getElement()).toHaveAttribute('aria-hidden', 'true');
});
test('has tab-index=-1', () => {
const breadCrumbGroup = renderBreadcrumbGroup({
items: [
{
text: 'Item 1',
href: '/#1',
},
{
text: 'Item 2',
href: '/#3',
},
{
text: 'Item 3',
href: '/#3',
},
],
});
expect(breadCrumbGroup.find(`.${styles.ghost}`)?.getElement()).toHaveAttribute('tabindex', '-1');
const anchors = breadCrumbGroup
.find(`.${styles.ghost}`)!
.findAll('a')
.map(wrapper => wrapper.getElement());
anchors.forEach(anchor => {
expect(anchor).toHaveAttribute('tabindex', '-1');
});
});
});
});
17 changes: 0 additions & 17 deletions src/breadcrumb-group/__tests__/breadcrumb-item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,8 @@ import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom';

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

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

const renderBreadcrumbGroup = (props: BreadcrumbGroupProps) => {
const { container } = render(<BreadcrumbGroup {...props} />);
return createWrapper(container).findBreadcrumbGroup()!;
Expand Down Expand Up @@ -136,16 +131,4 @@ describe('BreadcrumbGroup Item', () => {
expect(element.innerHTML).toBe(expectedFunnelName);
});
});

describe('compressed item', () => {
beforeEach(() => {
(useMobile as jest.Mock).mockReturnValue(true);
});

test('renders item content', () => {
const wrapper = renderBreadcrumbGroup({ items });

expect(wrapper.findBreadcrumbLinks()[0].getElement()).toHaveTextContent(items[0].text);
});
});
});
Loading

0 comments on commit 93a32ff

Please sign in to comment.