+ {ENABLE_FEEDBACK && (
+
+
+ Was this page helpful?
+
+
+ {Object.entries(feedbackButtons).map(([key, button]) => {
+ const isActive = feedbackMode === key;
+
+ return (
+
+ );
+ })}
+
+
+ )}
+ {feedbackMode &&
+ (() => {
+ const activeFeedbackButton = feedbackButtons[feedbackMode];
+
+ if (submitted) {
+ return (
+
+
+ Feedback submitted. Thank you!
+
+
+ );
+ }
+
+ return (
+
+ {activeFeedbackButton?.description && (
+
+ {activeFeedbackButton.description}
+
+ )}
+
+ );
+ })()}
+
+
+ {lastUpdated && (
+
+ Last updated: {lastUpdated}
+
+ )}
+ {githubEditPath && (
+
+
+ Edit on GitHub
- ))}
+ )}
-
- {[leftFooterLinks, rightFooterLinks].map((links, index) => (
-
- {links.map((link) => (
-
- {link.label}
-
+
+
+
+ {[leftFooterLinks, rightFooterLinks].map((links, index) => (
+
))}
- ))}
-
-
-);
+
+
+
+ );
+};
export default Footer;
diff --git a/src/components/Layout/Header.test.tsx b/src/components/Layout/Header.test.tsx
index 7fc0f3a681..26852a0cde 100644
--- a/src/components/Layout/Header.test.tsx
+++ b/src/components/Layout/Header.test.tsx
@@ -56,40 +56,19 @@ describe('Header', () => {
it('renders the header with logo and links', () => {
render(
);
- expect(screen.getAllByAltText('Ably logo').length).toBeGreaterThan(0);
+ expect(screen.getByAltText('Ably')).toBeInTheDocument();
- expect(screen.getByText('Docs')).toBeInTheDocument();
+ expect(screen.getAllByText('Docs')).toHaveLength(2);
expect(screen.getByText('Examples')).toBeInTheDocument();
});
- it('renders the search bar when searchBar is true', () => {
- render(
);
- expect(screen.getByText('SearchBar')).toBeInTheDocument();
- });
-
- it('does not render the search bar when searchBar is false', () => {
- render(
);
- expect(screen.queryByText('SearchBar')).not.toBeInTheDocument();
- });
-
it('toggles the mobile menu when the burger icon is clicked', () => {
render(
);
const burgerIcon = screen.getByText('icon-gui-bars-3-outline');
fireEvent.click(burgerIcon);
- expect(screen.getByText('icon-gui-x-mark-outline')).toBeInTheDocument();
expect(screen.getByText('LeftSidebar')).toBeInTheDocument();
});
- it('disables scrolling when the mobile menu is open', () => {
- render(
);
- const burgerIcon = screen.getByText('icon-gui-bars-3-outline');
- fireEvent.click(burgerIcon);
- expect(document.body).toHaveClass('overflow-hidden');
- const closeIcon = screen.getByText('icon-gui-x-mark-outline');
- fireEvent.click(closeIcon);
- expect(document.body).not.toHaveClass('overflow-hidden');
- });
-
it('renders the sign in buttons when not signed in', () => {
render(
diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx
index be8cddad38..8d0c3dd436 100644
--- a/src/components/Layout/Header.tsx
+++ b/src/components/Layout/Header.tsx
@@ -1,19 +1,57 @@
-import React, { useContext } from 'react';
+import React, { useContext, useEffect, useRef, useState } from 'react';
import { useLocation } from '@reach/router';
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
+import * as Tooltip from '@radix-ui/react-tooltip';
+import { throttle } from 'es-toolkit/compat';
import Icon from '@ably/ui/core/Icon';
-import AblyHeader from '@ably/ui/core/Header';
-import { SearchBar } from '../SearchBar';
+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 { IconName } from '@ably/ui/core/Icon/types';
import LeftSidebar from './LeftSidebar';
import UserContext from 'src/contexts/user-context';
import ExamplesList from '../Examples/ExamplesList';
-import TabMenu from '@ably/ui/core/TabMenu';
import Link from '../Link';
+import { InkeepSearchBar } from '../SearchBar/InkeepSearchBar';
+import { secondaryButtonClassName, iconButtonClassName, tooltipContentClassName } from './utils/styles';
-type HeaderProps = {
- searchBar?: boolean;
-};
+// Tailwind 'md' breakpoint from tailwind.config.js
+const MD_BREAKPOINT = 1040;
+const CLI_ENABLED = false;
+const MAX_MOBILE_MENU_WIDTH = '560px';
+
+const desktopTabs = [
+
+ Docs
+ ,
+
+ Examples
+ ,
+];
+
+const mobileTabs = ['Docs', 'Examples'];
+
+const helpResourcesItems = [
+ {
+ href: '/support',
+ icon: 'icon-gui-lifebuoy-outline' as IconName,
+ label: 'Support',
+ },
+ {
+ href: '/sdks',
+ icon: 'icon-gui-cube-outline' as IconName,
+ label: 'SDKs',
+ external: true,
+ },
+ {
+ href: 'https://ably.com',
+ icon: 'icon-gui-ably-badge' as IconName,
+ label: 'ably.com',
+ external: true,
+ },
+];
-const Header: React.FC = ({ searchBar = true }) => {
+const Header: React.FC = () => {
const location = useLocation();
const userContext = useContext(UserContext);
const sessionState = {
@@ -23,21 +61,57 @@ const Header: React.FC = ({ searchBar = true }) => {
accountName: userContext.sessionState.accountName ?? '',
account: userContext.sessionState.account ?? { links: { dashboard: { href: '#' } } },
};
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const mobileMenuRef = useRef(null);
+ const burgerButtonRef = useRef(null);
+ const searchBarRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ const clickedOutsideMenu = mobileMenuRef.current && !mobileMenuRef.current.contains(target);
+ const clickedOutsideBurgerButton = burgerButtonRef.current && !burgerButtonRef.current.contains(target);
+
+ if (isMobileMenuOpen && clickedOutsideMenu && clickedOutsideBurgerButton) {
+ setIsMobileMenuOpen(false);
+ }
+ };
+
+ const handleResize = throttle(() => {
+ if (window.innerWidth >= MD_BREAKPOINT && isMobileMenuOpen) {
+ setIsMobileMenuOpen(false);
+ }
+ }, 150);
+
+ // Physically shift the inkeep search bar around given that it's initialised once
+ const targetId = isMobileMenuOpen ? 'inkeep-search-mobile-mount' : 'inkeep-search-mount';
+ const targetElement = document.getElementById(targetId);
+ const searchBar = searchBarRef.current;
- const desktopTabs = [
-
- Docs
- ,
-
- Examples
- ,
- ];
- const mobileTabs = ['Docs', 'Examples'];
+ if (targetElement && searchBar) {
+ targetElement.appendChild(searchBar);
+ }
+
+ window.addEventListener('mousedown', handleClickOutside);
+ window.addEventListener('resize', handleResize);
+ return () => {
+ window.removeEventListener('mousedown', handleClickOutside);
+ window.removeEventListener('resize', handleResize);
+ handleResize.cancel();
+ };
+ }, [isMobileMenuOpen]);
return (
-
+
+
+
+
+ Docs
+
+
= ({ searchBar = true }) => {
defaultTabIndex: location.pathname.includes('/examples') ? 1 : 0,
}}
/>
- }
- mobileNav={
- ,
- ,
- ]}
- rootClassName="h-full overflow-y-hidden min-h-[3.1875rem] flex flex-col"
- contentClassName="h-full py-4 overflow-y-scroll"
- tabClassName="ui-text-menu2 !px-4"
- options={{ flexibleTabWidth: true }}
- />
- }
- searchButton={
-
+
+
+
- }
- searchButtonVisibility="mobile"
- searchBar={
- searchBar ? (
-
- ) : null
- }
- headerCenterClassName="flex-none w-52 lg:w-[17.5rem]"
- headerLinks={[
- {
- href: '/docs/sdks',
- label: 'SDKs',
- external: true,
- },
- {
- href: '/support',
- label: 'Support',
- },
- ]}
- sessionState={sessionState}
- logoHref="/docs"
- location={location}
- />
+ >
+
+
Ask AI
+
+
+
+
+
+
+
+
+
+ Help & Resources
+
+
+
+
+ {helpResourcesItems.map((item) => (
+
+
+
+
+ {item.label}
+
+ {item.external && }
+
+
+ ))}
+
+
+
+ {CLI_ENABLED && (
+
+
+
+
+
+ Open CLI
+
+
+ )}
+ {sessionState.signedIn ? (
+ <>
+ {sessionState.preferredEmail ? (
+
+
+
+ Dashboard
+
+
+
+ {sessionState.preferredEmail}
+
+
+ ) : (
+
+ Dashboard
+
+ )}
+
+
+
+
+
+ Log out
+
+
+ >
+ ) : (
+ <>
+
+ Login
+
+
+ Start free
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/components/Layout/LanguageSelector.test.tsx b/src/components/Layout/LanguageSelector.test.tsx
index 3514a496b3..049271c294 100644
--- a/src/components/Layout/LanguageSelector.test.tsx
+++ b/src/components/Layout/LanguageSelector.test.tsx
@@ -1,9 +1,10 @@
import React from 'react';
import { useLocation } from '@reach/router';
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { LanguageSelector } from './LanguageSelector';
import { useLayoutContext } from 'src/contexts/layout-context';
+import { navigate } from '../Link';
jest.mock('src/contexts/layout-context', () => ({
useLayoutContext: jest.fn(),
@@ -19,6 +20,10 @@ jest.mock('@ably/ui/core/Badge', () => ({
default: ({ children }: { children: React.ReactNode }) =>