Skip to content
Open
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@mdx-js/react": "^2.3.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-hook/media-query": "^1.1.1",
"@sentry/gatsby": "^9.19.0",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Article/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { FunctionComponent as FC } from 'react';
import { ArticleFooter } from './ArticleFooter';

const Article: FC<{ children: React.ReactNode }> = ({ children }) => (
<article className="flex-1 overflow-x-hidden relative z-10">
<article className="flex-1 overflow-x-hidden px-4 -mx-4 relative z-10">
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows focus outlines to be visible when otherwise they would be cut off by the container's overflow property

{children}
<ArticleFooter />
</article>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const Head = ({
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Manrope:[email protected]&family=Source+Code+Pro:wght@600&display=swap"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Manrope:[email protected]&family=Source+Code+Pro:wght@600&family=IBM+Plex+Serif:ital,wght@1,400;1,700&display=swap"
rel="stylesheet"
/>
</Helmet>
Expand Down
47 changes: 29 additions & 18 deletions src/components/Layout/Breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,50 +37,61 @@ describe('Breadcrumbs', () => {
jest.clearAllMocks();
});

it('renders relevant breadcrumb nodes', () => {
it('renders all breadcrumb nodes from activePage tree', () => {
render(<Breadcrumbs />);
expect(screen.getByText('Home')).toBeInTheDocument();
expect(screen.getByText('Section 1')).toBeInTheDocument();
expect(screen.queryByText('Subsection 1')).not.toBeInTheDocument();
expect(screen.getByText('Subsection 1')).toBeInTheDocument();
expect(screen.getByText('Current Page')).toBeInTheDocument();
});

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

it('disables the link for the current page', () => {
it('disables the link for the current page and non-linked nodes', () => {
render(<Breadcrumbs />);

// Current page (last item) should be disabled
expect(screen.getByText('Current Page')).toHaveClass('text-gui-unavailable');
expect(screen.getByText('Current Page')).toHaveClass('pointer-events-none');

// Non-linked nodes (link='#') should be disabled
expect(screen.getByText('Subsection 1')).toHaveClass('text-gui-unavailable');
expect(screen.getByText('Subsection 1')).toHaveClass('pointer-events-none');

// Active links should not be disabled
expect(screen.getByText('Section 1')).not.toHaveClass('text-gui-unavailable');
expect(screen.getByText('Section 1')).not.toHaveClass('pointer-events-none');
});

it('shows only the last active node in mobile view', () => {
render(<Breadcrumbs />);

// All items except index 0 should have 'hidden sm:flex' classes
expect(screen.getByText('Section 1')).not.toHaveClass('hidden');
expect(screen.getByText('Subsection 1')).toHaveClass('hidden', 'sm:flex');
expect(screen.getByText('Current Page')).toHaveClass('hidden', 'sm:flex');
});

it('removes duplicate links from breadcrumb nodes', () => {
it('correctly identifies last active node when current page is non-linked', () => {
mockUseLayoutContext.mockReturnValue({
activePage: {
tree: [
{ page: { name: 'Section 1', link: '/section-1' } },
{ page: { name: 'Duplicate Section', link: '/section-1' } },
{ page: { name: 'Current Page', link: '/section-1/page-1' } },
{ page: { name: 'Subsection 1', link: '/section-1/subsection-1' } },
{ page: { name: 'Current Page', link: '#' } },
],
},
});

render(<Breadcrumbs />);

// Should only show one instance of the duplicate link
const section1Links = screen.getAllByText('Section 1');
expect(section1Links).toHaveLength(1);

// Should not render the duplicate with different text
expect(screen.queryByText('Duplicate Section')).not.toBeInTheDocument();

// Should still render other breadcrumb elements
expect(screen.getByText('Home')).toBeInTheDocument();
expect(screen.getByText('Current Page')).toBeInTheDocument();
expect(screen.getByText('Section 1')).toHaveClass('hidden', 'sm:flex');
expect(screen.getByText('Subsection 1')).not.toHaveClass('hidden');
expect(screen.getByText('Current Page')).toHaveClass('hidden', 'sm:flex');
});
});
46 changes: 25 additions & 21 deletions src/components/Layout/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import React, { useMemo } from 'react';
import React from 'react';
import { useLayoutContext } from 'src/contexts/layout-context';
import Link from '../Link';
import Icon from '@ably/ui/core/Icon';
import cn from '@ably/ui/core/utils/cn';
import { hierarchicalKey, PageTreeNode } from './utils/nav';
import { hierarchicalKey } from './utils/nav';

const linkStyles =
'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';

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

const breadcrumbNodes = useMemo(() => {
const filteredNodes = activePage?.tree.filter((node) => node.page.link !== '#') ?? [];
const uniqueNodes = filteredNodes.reduce((acc: PageTreeNode[], current) => {
const isDuplicate = acc.some((item) => item.page.link === current.page.link);
if (!isDuplicate) {
acc.push(current);
}
return acc;
}, []);
return uniqueNodes;
}, [activePage?.tree]);

if (breadcrumbNodes.length === 0) {
if (!activePage?.tree || activePage.tree.length === 0) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need to do this processing anymore, we no longer care about eliminating duplicates

return null;
}

const linkStyles =
'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';
const lastActiveNodeIndex = (() => {
const index = activePage.tree
.toReversed()
.slice(1)
.findIndex((node) => node.page.link !== '#');

if (index !== -1) {
return activePage.tree.length - index - 2;
}

return null;
})();

console.log(lastActiveNodeIndex, activePage.tree);

return (
<nav aria-label="breadcrumb" className="flex mt-8 items-center gap-1">
Expand All @@ -35,17 +38,18 @@ const Breadcrumbs: React.FC = () => {
<Icon
name="icon-gui-chevron-right-micro"
size="16px"
additionalCSS={cn('rotate-180 sm:rotate-0', { 'hidden sm:flex': breadcrumbNodes.length === 1 })}
color="text-neutral-900 dark:text-neutral-400"
additionalCSS={cn('rotate-180 sm:rotate-0', { 'hidden sm:flex': lastActiveNodeIndex === null })}
/>
{breadcrumbNodes.map((node, index) => (
{activePage.tree.map((node, index) => (
<React.Fragment key={hierarchicalKey(node.page.link, index, activePage.tree)}>
{index > 0 ? <Icon name="icon-gui-chevron-right-micro" size="16px" additionalCSS="hidden sm:flex" /> : null}
<Link
to={node.page.link}
className={cn(linkStyles, {
'text-gui-unavailable dark:text-gui-unavailable-dark pointer-events-none':
index === breadcrumbNodes.length - 1,
'hidden sm:flex': index !== breadcrumbNodes.length - 2,
index === activePage.tree.length - 1 || node.page.link === '#',
'hidden sm:flex': index !== lastActiveNodeIndex,
})}
>
{node.page.name}
Expand Down
10 changes: 1 addition & 9 deletions src/components/Layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import Icon from '@ably/ui/core/Icon';
import TabMenu from '@ably/ui/core/TabMenu';
import Logo from '@ably/ui/core/images/logo/ably-logo.svg';
import { componentMaxHeight, HEADER_BOTTOM_MARGIN, HEADER_HEIGHT } from '@ably/ui/core/utils/heights';
import cn from '@ably/ui/core/utils/cn';
import { IconName } from '@ably/ui/core/Icon/types';
import LeftSidebar from './LeftSidebar';
import UserContext from 'src/contexts/user-context';
import ExamplesList from '../Examples/ExamplesList';
import Link from '../Link';
import { InkeepSearchBar } from '../SearchBar/InkeepSearchBar';
import { secondaryButtonClassName, iconButtonClassName, tooltipContentClassName } from './utils/styles';

// Tailwind 'md' breakpoint from tailwind.config.js
const MD_BREAKPOINT = 1040;
Expand Down Expand Up @@ -50,14 +50,6 @@ const helpResourcesItems = [
external: true,
},
];
const tooltipContentClassName = cn(
'px-2 py-1 bg-neutral-1000 dark:bg-neutral-300 text-neutral-200 dark:text-neutral-1100 ui-text-p3 font-medium rounded-lg relative z-50 mt-2',
'data-[state=closed]:animate-[tooltipExit_0.25s_ease-in-out]',
'data-[state=delayed-open]:animate-[tooltipEntry_0.25s_ease-in-out]',
);
const secondaryButtonClassName =
'focus-base flex items-center justify-center gap-2 px-4 py-[7px] h-9 ui-text-label4 text-neutral-1300 dark:text-neutral-000 rounded border border-neutral-400 dark:border-neutral-900 hover:border-neutral-600 dark:hover:border-neutral-700';
const iconButtonClassName = cn(secondaryButtonClassName, 'w-9 p-0');

const Header: React.FC = () => {
const location = useLocation();
Expand Down
Loading