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

[pull] main from nodejs:main #313

Merged
merged 4 commits into from
Nov 15, 2023
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 .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const rootClasses = classNames(

const preview: Preview = {
parameters: {
nextjs: { router: { basePath: '' } },
nextjs: { router: { basePath: '' }, appDirectory: true },
chromatic: { modes: STORYBOOK_MODES },
viewport: { defaultViewport: 'large', viewports: STORYBOOK_SIZES },
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { render, screen } from '@testing-library/react';

import ActiveLocalizedLink from '..';
import ActiveLink from '..';

describe('ActiveLocalizedLink', () => {
describe('ActiveLink', () => {
it('renders as localized link', () => {
render(
<ActiveLocalizedLink
className="link"
activeClassName="active"
href="/link"
>
<ActiveLink className="link" activeClassName="active" href="/link">
Link
</ActiveLocalizedLink>
</ActiveLink>
);

expect(screen.findByText('Link')).resolves.toHaveAttribute(
Expand All @@ -22,27 +18,19 @@ describe('ActiveLocalizedLink', () => {

it('ignores active class when href not matches location.href', () => {
render(
<ActiveLocalizedLink
className="link"
activeClassName="active"
href="/not-link"
>
<ActiveLink className="link" activeClassName="active" href="/not-link">
Link
</ActiveLocalizedLink>
</ActiveLink>
);

expect(screen.findByText('Link')).resolves.toHaveAttribute('class', 'link');
});

it('sets active class when href matches location.href', () => {
render(
<ActiveLocalizedLink
className="link"
activeClassName="active"
href="/link"
>
<ActiveLink className="link" activeClassName="active" href="/link">
Link
</ActiveLocalizedLink>
</ActiveLink>
);

expect(screen.findByText('Link')).resolves.toHaveAttribute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@ import Link from '@/components/Link';
import { usePathname } from '@/navigation.mjs';

type ActiveLocalizedLinkProps = ComponentProps<typeof Link> & {
activeClassName: string;
activeClassName?: string;
allowSubPath?: boolean;
};

const ActiveLocalizedLink: FC<ActiveLocalizedLinkProps> = ({
const ActiveLink: FC<ActiveLocalizedLinkProps> = ({
children,
activeClassName,
activeClassName = 'active',
allowSubPath = false,
className,
href = '',
...props
}) => {
const pathname = usePathname();

const linkURL = new URL(href.toString(), location.href);

const finalClassName = classNames(className, {
[activeClassName]: linkURL.pathname === pathname,
[activeClassName]: allowSubPath
? pathname.startsWith(href.toString())
: href.toString() === pathname,
});

return (
Expand All @@ -32,4 +34,4 @@ const ActiveLocalizedLink: FC<ActiveLocalizedLinkProps> = ({
);
};

export default ActiveLocalizedLink;
export default ActiveLink;
2 changes: 1 addition & 1 deletion components/Common/LanguageDropDown/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const Default: Story = {
{ name: 'French', code: 'fr' },
{ name: 'Spanish', code: 'es' },
],
currentLanguage: { name: 'English', code: 'en' },
currentLanguage: 'en',
},
};

Expand Down
4 changes: 2 additions & 2 deletions components/Common/LanguageDropDown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type SimpleLocaleConfig = Pick<LocaleConfig, 'name' | 'code'>;

type LanguageDropDownProps = {
onChange?: (newLocale: SimpleLocaleConfig) => void;
currentLanguage: SimpleLocaleConfig;
currentLanguage: string;
availableLanguages: SimpleLocaleConfig[];
};

Expand Down Expand Up @@ -45,7 +45,7 @@ const LanguageDropdown: FC<LanguageDropDownProps> = ({
key={code}
onClick={() => onChange({ name, code })}
className={classNames(styles.dropDownItem, {
[styles.currentDropDown]: code === currentLanguage.code,
[styles.currentDropDown]: code === currentLanguage,
})}
>
{name}
Expand Down
8 changes: 4 additions & 4 deletions components/Containers/NavItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from 'classnames';
import type { FC, PropsWithChildren } from 'react';
import { useMemo } from 'react';

import ActiveLocalizedLink from '@/components/Common/ActiveLocalizedLink';
import ActiveLink from '@/components/Common/ActiveLink';

import styles from './index.module.css';

Expand All @@ -22,19 +22,19 @@ const NavItem: FC<PropsWithChildren<NavItemProps>> = ({
className,
}) => {
const showIcon = useMemo(
() => type === 'nav' && !href.toString().startsWith('/'),
() => type === 'nav' && href.toString().startsWith('http'),
[href, type]
);

return (
<ActiveLocalizedLink
<ActiveLink
href={href}
className={classNames(styles.navItem, styles[type], className)}
activeClassName={styles.active}
>
<span className={styles.label}>{children}</span>
{showIcon && <ArrowUpRightIcon className={styles.icon} />}
</ActiveLocalizedLink>
</ActiveLink>
);
};

Expand Down
108 changes: 108 additions & 0 deletions components/Containers/NavigationBar/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
.container {
@apply border-neutral-200
dark:border-neutral-900
md:flex
md:h-16
md:flex-row
md:items-center
md:gap-8
md:border-b
md:px-8;
}

.nodeIconAndMobileItemsToggler {
@apply flex
h-16
shrink-0
items-center
border-b
border-neutral-200
px-4
dark:border-neutral-900
md:flex
md:h-full
md:items-center
md:border-0
md:px-0;
}

.nodeIconWrapper {
@apply h-[30px]
flex-1;
}

.nodejsLogoDark {
@apply h-6
w-20
dark:hidden;
}

.nodejsLogoLight {
@apply hidden
h-6
w-20
dark:block;
}

.navInteractionIcon {
@apply h-6
w-6;
}

.sidebarItemTogglerLabel {
@apply block
cursor-pointer
md:hidden;
}

.main {
@apply hidden
flex-1
flex-col
md:flex
md:flex-row
md:items-center;
}

.navItems {
@apply flex
flex-col
gap-1
border-b
border-neutral-200
p-4
dark:border-neutral-900
md:flex-1
md:flex-row
md:border-0
md:p-0;
}

.actionsWrapper {
@apply flex
items-center
gap-2
border-b
border-neutral-200
p-4
dark:border-neutral-900
md:border-0
md:p-0;
}

.ghIconWrapper {
@apply h-9
w-9
rounded-md
p-2;

svg {
@apply fill-neutral-700
dark:fill-neutral-300;
}

&:hover {
@apply bg-neutral-100
dark:bg-neutral-900;
}
}
48 changes: 48 additions & 0 deletions components/Containers/NavigationBar/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Meta as MetaObj, StoryObj } from '@storybook/react';

import NavigationBar from './index';

type Story = StoryObj<typeof NavigationBar>;
type Meta = MetaObj<typeof NavigationBar>;

export const Default: Story = {
args: {
navItems: [
{
text: 'Learn',
href: '/learn',
},
{
text: 'About',
href: '/about',
},
{
text: 'Docs',
href: '/docs',
},
{
text: 'Download',
href: '/download',
},
{
text: 'Blog',
href: '/blog',
},
{
text: 'Certification',
href: 'https://openjsf.org/certification',
},
],
languages: {
availableLanguages: [
{ name: 'English', code: 'en' },
{ name: 'French', code: 'fr' },
{ name: 'Spanish', code: 'es' },
],
currentLanguage: 'en',
},
onThemeTogglerClick: () => {},
},
};

export default { component: NavigationBar } as Meta;
80 changes: 80 additions & 0 deletions components/Containers/NavigationBar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';

import Hamburger from '@heroicons/react/24/solid/Bars3Icon';
import XMark from '@heroicons/react/24/solid/XMarkIcon';
import * as Label from '@radix-ui/react-label';
import { useState } from 'react';
import type { FC, ComponentProps } from 'react';

import LanguageDropdown from '@/components/Common/LanguageDropDown';
import ThemeToggle from '@/components/Common/ThemeToggle';
import NavItem from '@/components/Containers/NavItem';
import GithubLogo from '@/components/Icons/GitHubLogo';
import NodejsLogoDark from '@/components/Icons/NodejsLogoDark';
import NodejsLogoLight from '@/components/Icons/NodejsLogoLight';

import style from './index.module.css';

type NavItem = { text: string; href: string };

type NavbarProps = {
navItems: NavItem[];
languages: ComponentProps<typeof LanguageDropdown>;
onThemeTogglerClick: () => void;
};

const navInteractionIcons = {
show: <Hamburger className={style.navInteractionIcon} />,
close: <XMark className={style.navInteractionIcon} />,
};

const NavigationBar: FC<NavbarProps> = ({
navItems,
languages,
onThemeTogglerClick,
}) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<nav className={`${style.container}`}>
<div className={style.nodeIconAndMobileItemsToggler}>
<div className={style.nodeIconWrapper}>
<NodejsLogoDark className={style.nodejsLogoDark} />
<NodejsLogoLight className={style.nodejsLogoLight} />
</div>
<Label.Root
onClick={() => setIsMenuOpen(prev => !prev)}
className={style.sidebarItemTogglerLabel}
htmlFor="sidebarItemToggler"
>
{navInteractionIcons[isMenuOpen ? 'close' : 'show']}
</Label.Root>
</div>
<input className="peer hidden" id="sidebarItemToggler" type="checkbox" />
<div className={`${style.main} peer-checked:flex`}>
<div className={style.navItems}>
{navItems.map(({ text, href }) => (
<NavItem key={text} href={href}>
{text}
</NavItem>
))}
</div>
<div className={style.actionsWrapper}>
<ThemeToggle onClick={onThemeTogglerClick} />
<LanguageDropdown
availableLanguages={languages.availableLanguages}
currentLanguage={languages.currentLanguage}
/>
<a
className={style.ghIconWrapper}
href="https://github.com/nodejs/node"
aria-label="Node.js Github"
>
<GithubLogo />
</a>
</div>
</div>
</nav>
);
};

export default NavigationBar;
Loading
Loading