Skip to content

Commit dee744e

Browse files
committed
fix: update breadcrumbs behaviour
1 parent 0a2af33 commit dee744e

File tree

2 files changed

+41
-39
lines changed

2 files changed

+41
-39
lines changed

src/components/Layout/Breadcrumbs.test.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,50 +37,61 @@ describe('Breadcrumbs', () => {
3737
jest.clearAllMocks();
3838
});
3939

40-
it('renders relevant breadcrumb nodes', () => {
40+
it('renders all breadcrumb nodes from activePage tree', () => {
4141
render(<Breadcrumbs />);
4242
expect(screen.getByText('Home')).toBeInTheDocument();
4343
expect(screen.getByText('Section 1')).toBeInTheDocument();
44-
expect(screen.queryByText('Subsection 1')).not.toBeInTheDocument();
44+
expect(screen.getByText('Subsection 1')).toBeInTheDocument();
4545
expect(screen.getByText('Current Page')).toBeInTheDocument();
4646
});
4747

4848
it('includes relevant links on the breadcrumb nodes', () => {
4949
render(<Breadcrumbs />);
5050
expect(screen.getByText('Home')).toHaveAttribute('href', '/docs');
5151
expect(screen.getByText('Section 1')).toHaveAttribute('href', '/section-1');
52-
expect(screen.queryByText('Subsection 1')).not.toBeInTheDocument();
52+
expect(screen.getByText('Subsection 1')).toHaveAttribute('href', '#');
5353
expect(screen.getByText('Current Page')).toHaveAttribute('href', '/section-1/subsection-1/page-1');
5454
});
5555

56-
it('disables the link for the current page', () => {
56+
it('disables the link for the current page and non-linked nodes', () => {
5757
render(<Breadcrumbs />);
58+
59+
// Current page (last item) should be disabled
5860
expect(screen.getByText('Current Page')).toHaveClass('text-gui-unavailable');
5961
expect(screen.getByText('Current Page')).toHaveClass('pointer-events-none');
62+
63+
// Non-linked nodes (link='#') should be disabled
64+
expect(screen.getByText('Subsection 1')).toHaveClass('text-gui-unavailable');
65+
expect(screen.getByText('Subsection 1')).toHaveClass('pointer-events-none');
66+
67+
// Active links should not be disabled
68+
expect(screen.getByText('Section 1')).not.toHaveClass('text-gui-unavailable');
69+
expect(screen.getByText('Section 1')).not.toHaveClass('pointer-events-none');
70+
});
71+
72+
it('shows only the last active node in mobile view', () => {
73+
render(<Breadcrumbs />);
74+
75+
// All items except index 0 should have 'hidden sm:flex' classes
76+
expect(screen.getByText('Section 1')).not.toHaveClass('hidden');
77+
expect(screen.getByText('Subsection 1')).toHaveClass('hidden', 'sm:flex');
78+
expect(screen.getByText('Current Page')).toHaveClass('hidden', 'sm:flex');
6079
});
6180

62-
it('removes duplicate links from breadcrumb nodes', () => {
81+
it('correctly identifies last active node when current page is non-linked', () => {
6382
mockUseLayoutContext.mockReturnValue({
6483
activePage: {
6584
tree: [
6685
{ page: { name: 'Section 1', link: '/section-1' } },
67-
{ page: { name: 'Duplicate Section', link: '/section-1' } },
68-
{ page: { name: 'Current Page', link: '/section-1/page-1' } },
86+
{ page: { name: 'Subsection 1', link: '/section-1/subsection-1' } },
87+
{ page: { name: 'Current Page', link: '#' } },
6988
],
7089
},
7190
});
7291

7392
render(<Breadcrumbs />);
74-
75-
// Should only show one instance of the duplicate link
76-
const section1Links = screen.getAllByText('Section 1');
77-
expect(section1Links).toHaveLength(1);
78-
79-
// Should not render the duplicate with different text
80-
expect(screen.queryByText('Duplicate Section')).not.toBeInTheDocument();
81-
82-
// Should still render other breadcrumb elements
83-
expect(screen.getByText('Home')).toBeInTheDocument();
84-
expect(screen.getByText('Current Page')).toBeInTheDocument();
93+
expect(screen.getByText('Section 1')).toHaveClass('hidden', 'sm:flex');
94+
expect(screen.getByText('Subsection 1')).not.toHaveClass('hidden');
95+
expect(screen.getByText('Current Page')).toHaveClass('hidden', 'sm:flex');
8596
});
8697
});

src/components/Layout/Breadcrumbs.tsx

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,21 @@
1-
import React, { useMemo } from 'react';
1+
import React from 'react';
22
import { useLayoutContext } from 'src/contexts/layout-context';
33
import Link from '../Link';
44
import Icon from '@ably/ui/core/Icon';
55
import cn from '@ably/ui/core/utils/cn';
6-
import { hierarchicalKey, PageTreeNode } from './utils/nav';
6+
import { hierarchicalKey } from './utils/nav';
7+
8+
const linkStyles =
9+
'ui-text-label4 font-semibold text-neutral-900 hover:text-neutral-1300 active:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-000 dark:active:text-neutral-500 focus-base transition-colors';
710

811
const Breadcrumbs: React.FC = () => {
912
const { activePage } = useLayoutContext();
1013

11-
const breadcrumbNodes = useMemo(() => {
12-
const filteredNodes = activePage?.tree.filter((node) => node.page.link !== '#') ?? [];
13-
const uniqueNodes = filteredNodes.reduce((acc: PageTreeNode[], current) => {
14-
const isDuplicate = acc.some((item) => item.page.link === current.page.link);
15-
if (!isDuplicate) {
16-
acc.push(current);
17-
}
18-
return acc;
19-
}, []);
20-
return uniqueNodes;
21-
}, [activePage?.tree]);
22-
23-
if (breadcrumbNodes.length === 0) {
14+
if (!activePage?.tree || activePage.tree.length === 0) {
2415
return null;
2516
}
2617

27-
const linkStyles =
28-
'ui-text-label4 font-semibold text-neutral-900 hover:text-neutral-1300 active:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-000 dark:active:text-neutral-500';
18+
const lastActiveNodeIndex = activePage.tree.toReversed().findIndex((node) => node.page.link !== '#') ?? 0;
2919

3020
return (
3121
<nav aria-label="breadcrumb" className="flex mt-8 items-center gap-1">
@@ -35,17 +25,18 @@ const Breadcrumbs: React.FC = () => {
3525
<Icon
3626
name="icon-gui-chevron-right-micro"
3727
size="16px"
38-
additionalCSS={cn('rotate-180 sm:rotate-0', { 'hidden sm:flex': breadcrumbNodes.length === 1 })}
28+
color="text-neutral-900 dark:text-neutral-400"
29+
additionalCSS={cn('rotate-180 sm:rotate-0', { 'hidden sm:flex': activePage.tree.length === 1 })}
3930
/>
40-
{breadcrumbNodes.map((node, index) => (
31+
{activePage.tree.map((node, index) => (
4132
<React.Fragment key={hierarchicalKey(node.page.link, index, activePage.tree)}>
4233
{index > 0 ? <Icon name="icon-gui-chevron-right-micro" size="16px" additionalCSS="hidden sm:flex" /> : null}
4334
<Link
4435
to={node.page.link}
4536
className={cn(linkStyles, {
4637
'text-gui-unavailable dark:text-gui-unavailable-dark pointer-events-none':
47-
index === breadcrumbNodes.length - 1,
48-
'hidden sm:flex': index !== breadcrumbNodes.length - 2,
38+
index === activePage.tree.length - 1 || node.page.link === '#',
39+
'hidden sm:flex': index !== lastActiveNodeIndex,
4940
})}
5041
>
5142
{node.page.name}

0 commit comments

Comments
 (0)