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

feat: Improve breadcrumb group responsivness #3025

Merged
merged 17 commits into from
Dec 9, 2024
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
2 changes: 1 addition & 1 deletion pages/breadcrumb-group/responsive.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function ResponsiveBreadcrumbsPage() {
<ResponsiveBreadcrumbs
widths={[900, 800, 700, 600, 500, 400, 300, 200]}
items={[
'A',
'Amazon service name',
'Longer breadrcumb',
'ABC',
'Another even longer breadcrumb',
Expand Down
189 changes: 41 additions & 148 deletions src/breadcrumb-group/__integ__/breadcrumb-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,17 @@ const setupTest = (
});
};
describe('BreadcrumbGroup', () => {
test(
'Has proper number of items in the dropdown',
test.each([
[{ width: 770, height: 800 }, 1],
[{ width: 740, height: 800 }, 2],
[{ width: 680, height: 800 }, 3],
[{ width: 610, height: 800 }, 4],
[{ width: 550, height: 800 }, 6],
])('Has proper number of items in the dropdown: %o %d', (sizes, itemsInDropdown) =>
setupTest(async page => {
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);
})
expect(await page.getElementsCount(dropdownItemsSelector)).toBe(itemsInDropdown);
}, sizes)()
);
test(
'Does not return ghost items',
Expand All @@ -105,7 +94,7 @@ describe('BreadcrumbGroup', () => {
test(
'Adjusts display when adding/removing items',
setupTest(async page => {
await page.setWindowSize({ width: 700, height: 800 });
await page.setWindowSize({ width: 950, height: 800 });
expect(page.isEllipsisVisible()).resolves.toBe(false);
await page.click('#add');
expect(page.isEllipsisVisible()).resolves.toBe(true);
Expand Down Expand Up @@ -134,104 +123,35 @@ describe('BreadcrumbGroup', () => {
await page.openDropdown();
await expect(page.getText('#onFollowMessage')).resolves.toEqual('');
await expect(page.getText('#onClickMessage')).resolves.toEqual('');
await page.clickItem(1);
await page.clickItem(2);
await expect(page.getText('#onFollowMessage')).resolves.toEqual('OnFollow: Second item was selected');
await expect(page.getText('#onClickMessage')).resolves.toEqual('OnClick: Second item was selected');
})
);

describe('Item popover', () => {
describe.each([[true], [false]])('analytics attributes (mobile: %p)', mobile => {
test(
'should be displayed for truncated items on first render',
setupTest(
async page => {
await page.click('#focus-target-long-text');
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(true);
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(false);
},
{ width: 100, height: 800 }
)
);

test(
'should not be displayed for non-truncated items on first render',
setupTest(
async page => {
await page.click('#focus-target-long-text');
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(false);
},
{ width: 1200, height: 800 }
)
);

test(
'should be displayed for truncated items after resizing',
setupTest(
async page => {
await page.setWindowSize({ width: 1000, height: 800 });
await page.click('#focus-target-long-text');
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(true);
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(false);
},
{ width: 1200, height: 800 }
)
);
test(
'should be displayed for truncated items after collapsing items into dropdown',
setupTest(
async page => {
'attaches funnel name attribute',
setupTest(async (page, browser) => {
if (mobile) {
await page.setMobileViewport();
await page.click('#focus-target-long-text');
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(true);
},
{ width: 1200, height: 800 }
)
}
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(
'should not be displayed after making the viewport larger again',
setupTest(
async page => {
'attaches resource name attribute',
setupTest(async (page, browser) => {
if (mobile) {
await page.setMobileViewport();
await page.setWindowSize({ width: 1200, height: 800 });
await page.click('#focus-target-long-text');
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(false);
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(false);
await page.keys('Tab');
},
{ width: 1200, height: 800 }
)
);

test(
'Item popover should close after pressing Escape',
setupTest(async page => {
await page.setMobileViewport();
await page.click('#focus-target-long-text');
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(true);
await page.keys('Escape');
await expect(page.isTooltipDisplayed()).resolves.toBe(false);
}
const funnelName = await browser.$('[data-analytics-funnel-resource-type="true"]').getHTML();
expect(funnelName).toMatch('>Second<');
})
);
});

test(
'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(
Expand All @@ -251,48 +171,21 @@ describe('BreadcrumbGroup', () => {

test(
'Last item is focusable when truncated',
setupTest(async page => {
await page.setMobileViewport();
await page.click('#focus-target-long-text');
await page.keys('Tab');
await page.keys('Tab');
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(true);
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(false);
await expect(page.getActiveElementId()).resolves.toBe('focus-target-short-text');
})
);

test(
'Displays only one tooltip at the time',
setupTest(async page => {
await page.setMobileViewport();
await page.click('#focus-target-long-text');
await page.keys('Tab');
await expect(page.countTooltips()).resolves.toBe(1);
await expect(page.getTooltipText()).resolves.toBe(
'First that is very very very very very very long long long text'
);
await page.hoverElement(breadcrumbGroupWrapper.findBreadcrumbLink(6).toSelector());
await expect(page.countTooltips()).resolves.toBe(1);
await expect(page.getTooltipText()).resolves.toBe(
'Sixth that is very very very very very very long long long text'
);

await page.hoverElement('#focus-target-long-text');
await page.click('#focus-target-long-text');
await expect(page.countTooltips()).resolves.toBe(0);
await page.hoverElement(breadcrumbGroupWrapper.findBreadcrumbLink(6).toSelector());
await expect(page.countTooltips()).resolves.toBe(1);
await expect(page.getTooltipText()).resolves.toBe(
'Sixth that is very very very very very very long long long text'
);
await page.keys('Tab');
await expect(page.countTooltips()).resolves.toBe(1);
await expect(page.getTooltipText()).resolves.toBe(
'First that is very very very very very very long long long text'
);
})
setupTest(
async page => {
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.isTooltipDisplayed()).resolves.toBe(true);
await page.keys('Tab');
await expect(page.isTooltipDisplayed()).resolves.toBe(false);
await expect(page.getActiveElementId()).resolves.toBe('focus-target-short-text');
},
{ width: 950, height: 800 }
)
);
});
67 changes: 64 additions & 3 deletions src/breadcrumb-group/__tests__/breadcrumb-group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ import createWrapper, { BreadcrumbGroupWrapper, ElementWrapper } from '../../../
import itemStyles from '../../../lib/components/breadcrumb-group/item/styles.css.js';
import styles from '../../../lib/components/breadcrumb-group/styles.css.js';

let mockMobileViewport = false;
jest.mock('@cloudscape-design/component-toolkit', () => {
return {
...jest.requireActual('@cloudscape-design/component-toolkit'),
useContainerQuery: () => (mockMobileViewport ? [10, () => {}] : [9999, () => {}]),
};
});
jest.mock('@cloudscape-design/component-toolkit/internal', () => ({
...jest.requireActual('@cloudscape-design/component-toolkit/internal'),
getLogicalBoundingClientRect: () => ({ inlineSize: 50 }),
}));
afterEach(() => {
mockMobileViewport = false;
});

const renderBreadcrumbGroup = (props: BreadcrumbGroupProps) => {
const renderResult = render(<BreadcrumbGroup {...props} />);
return createWrapper(renderResult.container).findBreadcrumbGroup()!;
Expand Down Expand Up @@ -70,6 +85,27 @@ describe('BreadcrumbGroup Component', () => {
});
});

test('renders with items (mobile)', () => {
mockMobileViewport = true;
wrapper = renderBreadcrumbGroup({ items });
expect(wrapper.getElement().nodeName).toBe('NAV');

const dropdown = wrapper.findDropdown()!;
dropdown.openDropdown();
const links = dropdown.findItems();
expect(links).toHaveLength(3);

links.forEach((link, i) => {
expect(link.getElement()).toHaveTextContent(items[i].text);
if (i === links.length - 1) {
// last item should not have an href
expect(link.getElement().querySelector('a')).toBeFalsy();
} else {
expect(link.getElement().querySelector('a')).toHaveAttribute('href', items[i].href);
}
});
});

test('has ellipsis', () => {
expect(wrapper.findDropdown()!.findNativeButton()).not.toBe(null);
expect(wrapper.findByClassName(styles.ellipsis)!.getElement()).toBeInTheDocument();
Expand Down Expand Up @@ -121,9 +157,25 @@ describe('BreadcrumbGroup Component', () => {
rerender(<BreadcrumbGroup items={items.slice()} />);
expect(getIcons()).toHaveLength(2);
});

test('clicking current page in mobile dropdown should close dropdown without events', () => {
mockMobileViewport = true;
const onClick = jest.fn();
const onFollow = jest.fn();
const { container } = render(<BreadcrumbGroup items={items} onClick={onClick} onFollow={onFollow} />);
const wrapper = createWrapper(container).findBreadcrumbGroup()!;
const dropdown = wrapper.findDropdown()!;
dropdown.openDropdown();
expect(dropdown.findItems().length).toBe(3);
dropdown.findItems()[2].click();
expect(dropdown.findOpenDropdown()).toBeFalsy();
expect(onClick).not.toHaveBeenCalled();
expect(onFollow).not.toHaveBeenCalled();
});
});

test('supports extended items object', () => {
test.each([[true], [false]])('supports extended items object (mobile: %p)', mobile => {
mockMobileViewport = mobile;
interface ExtendedItem extends BreadcrumbGroupProps.Item {
metadata: number;
}
Expand All @@ -143,7 +195,13 @@ describe('BreadcrumbGroup Component', () => {
/>
);
const wrapper = createWrapper(container).findBreadcrumbGroup()!;
wrapper.findBreadcrumbLink(2)!.click();
if (mobile) {
const dropdown = wrapper.findDropdown()!;
dropdown.openDropdown();
dropdown.findItems()[1].click();
} else {
wrapper.findBreadcrumbLink(2)!.click();
}
expect(onClick).toHaveBeenCalledWith(items[1]);
});

Expand Down Expand Up @@ -233,7 +291,10 @@ describe('BreadcrumbGroup Component', () => {
});
});

describe('funnel attributes', () => {
describe.each([[true], [false]])('funnel attributes (mobile: %p)', mobile => {
beforeEach(() => {
mockMobileViewport = mobile;
});
function getElementsText(elements: Array<ElementWrapper>) {
return Array.from(elements).map(element => element.getElement().textContent);
}
Expand Down
Loading
Loading