Skip to content

Commit

Permalink
Fix the Navigation types when generating links. (#786)
Browse files Browse the repository at this point in the history
* Fix the Navigation types when generating links.
  • Loading branch information
huwshimi authored Jun 7, 2022
1 parent 8f29288 commit 45b0e9e
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 85 deletions.
1 change: 1 addition & 0 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{
"root": ["./src"],
"alias": {
"enums": "./src/enums",
"components": "./src/components",
"hooks": "./src/hooks",
"types": "./src/types",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Navigation/Navigation.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import Navigation from "./Navigation";
import { Theme } from "../../types";
import { Theme } from "../../enums";

<Meta title="Navigation" component={Navigation} />

Expand Down
19 changes: 13 additions & 6 deletions src/components/Navigation/Navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { fireEvent, render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import Navigation from "./Navigation";
import { Theme } from "../../types";
import { Theme } from "../../enums";
import { isNavigationAnchor } from "../../utils";

/* eslint-disable testing-library/no-node-access */
it("displays light theme", () => {
Expand Down Expand Up @@ -75,11 +76,17 @@ it("can display a standard logo", () => {
it("can display a standard logo with a generated link", () => {
render(
<Navigation
generateLink={({ url, label, isSelected, ...props }) => (
<a {...props} aria-current="page" href={url}>
{label}
</a>
)}
generateLink={(link) => {
if (isNavigationAnchor(link)) {
const { url, label, isSelected, ...props } = link;
return (
<a {...props} aria-current="page" href={url}>
{label}
</a>
);
}
return null;
}}
logo={{
"aria-label": "Homepage",
src: "http://this.is.the.logo.svg",
Expand Down
29 changes: 17 additions & 12 deletions src/components/Navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import classNames from "classnames";
import NavigationLink from "./NavigationLink";
import NavigationMenu from "./NavigationMenu";
import type { GenerateLink, NavItem, NavMenu, LogoProps } from "./types";
import { PropsWithSpread, SubComponentProps, Theme } from "types";
import { PropsWithSpread, SubComponentProps } from "types";
import SearchBox, { SearchBoxProps } from "components/SearchBox";
import { useOnEscapePressed } from "hooks";
import { Theme } from "enums";

export type Props = PropsWithSpread<
{
Expand Down Expand Up @@ -102,12 +103,14 @@ const generateLogo = (logo: Props["logo"], generateLink: GenerateLink) => {
return (
<div className="p-navigation__tagged-logo" {...logoProps}>
<NavigationLink
className="p-navigation__link"
url={url}
label={content}
aria-label={ariaLabel}
generateLink={generateLink}
isSelected={!!ariaCurrent}
link={{
"aria-label": ariaLabel,
className: "p-navigation__link",
isSelected: !!ariaCurrent,
label: content,
url: url,
}}
/>
</div>
);
Expand Down Expand Up @@ -139,13 +142,15 @@ const generateItems = (
key={i}
>
<NavigationLink
{...item}
onClick={(evt) => {
item.onClick?.(evt);
closeMobileMenu();
}}
generateLink={generateLink}
className={classNames("p-navigation__link", item.className)}
link={{
...item,
className: classNames("p-navigation__link", item.className),
onClick: (evt) => {
item.onClick?.(evt);
closeMobileMenu();
},
}}
/>
</li>
)
Expand Down
24 changes: 17 additions & 7 deletions src/components/Navigation/NavigationLink/NavigationLink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ import { render, screen } from "@testing-library/react";
import NavigationLink from "./NavigationLink";

it("generates a standard anchor", () => {
render(<NavigationLink label="Go here" url="/to/here" />);
render(<NavigationLink link={{ label: "Go here", url: "/to/here" }} />);
expect(
screen.getByRole("link", {
name: "Go here",
})
).toBeInTheDocument();
});

it("generates a button", () => {
render(<NavigationLink link={{ label: "Go here" }} />);
expect(
screen.getByRole("button", {
name: "Go here",
})
).toBeInTheDocument();
});

it("can select an anchor", () => {
render(<NavigationLink isSelected label="Go here" url="/to/here" />);
render(
<NavigationLink
link={{ isSelected: true, label: "Go here", url: "/to/here" }}
/>
);
expect(
screen.getByRole("link", {
name: "Go here",
Expand All @@ -26,8 +39,7 @@ it("generates a custom link", () => {
render(
<NavigationLink
generateLink={({ label }) => <button>{label}</button>}
label="Go here"
url="/to/here"
link={{ label: "Go here", url: "/to/here" }}
/>
);
expect(
Expand All @@ -43,9 +55,7 @@ it("can select a custom link", () => {
generateLink={({ label, ...props }) => (
<button aria-current={props["aria-current"]}>{label}</button>
)}
isSelected
label="Go here"
url="/to/here"
link={{ isSelected: true, label: "Go here", url: "/to/here" }}
/>
);
expect(
Expand Down
48 changes: 25 additions & 23 deletions src/components/Navigation/NavigationLink/NavigationLink.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,50 @@
import type { HTMLProps } from "react";
import React from "react";
import { PropsWithSpread } from "types";
import { isNavigationAnchor, isNavigationButton } from "utils";

import type { GenerateLink, NavLink } from "../types";

type Props = PropsWithSpread<
NavLink & {
generateLink?: GenerateLink;
},
HTMLProps<HTMLAnchorElement>
>;
type Props = {
generateLink?: GenerateLink;
link: NavLink;
};

/**
* This component is used internally to display links inside the Navigation component.
*/
const NavigationLink = ({
generateLink,
isSelected,
label,
url,
...props
}: Props): JSX.Element => {
const ariaCurrent = isSelected ? "page" : undefined;
const NavigationLink = ({ generateLink, link }: Props): JSX.Element | null => {
// const ariaCurrent = isSelected ? "page" : undefined;
if (generateLink) {
const { isSelected, ...linkProps } = link;
// If a function has been provided then use it to generate the link element.
return (
<>
{generateLink({
isSelected,
label,
url,
"aria-current": ariaCurrent,
...props,
"aria-current": isSelected ? "page" : undefined,
...linkProps,
})}
</>
);
} else {
// If a function has not been provided then use a standard anchor element.
} else if (isNavigationAnchor(link)) {
const { isSelected, label, url, ...linkProps } = link;
return (
<a href={url} {...props} aria-current={ariaCurrent}>
<a
{...linkProps}
href={url}
aria-current={isSelected ? "page" : undefined}
>
{label}
</a>
);
} else if (isNavigationButton(link)) {
const { isSelected, label, url, ...linkProps } = link;
return (
<button {...linkProps} aria-current={isSelected ? "page" : undefined}>
{label}
</button>
);
}
return null;
};

export default NavigationLink;
12 changes: 7 additions & 5 deletions src/components/Navigation/NavigationMenu/NavigationMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@ const NavigationMenu = ({
{items.map((item, i) => (
<li key={i}>
<NavigationLink
{...item}
generateLink={generateLink}
className={classNames(
"p-navigation__dropdown-item",
item.className
)}
link={{
...item,
className: classNames(
"p-navigation__dropdown-item",
item.className
),
}}
/>
</li>
))}
Expand Down
3 changes: 3 additions & 0 deletions src/components/Navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export type {
GenerateLink,
LogoProps,
NavLink,
NavLinkAnchor,
NavLinkBase,
NavLinkButton,
NavMenu,
NavItem,
} from "./types";
45 changes: 29 additions & 16 deletions src/components/Navigation/types.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import type { HTMLProps, ReactNode } from "react";
import type { HTMLAttributes, HTMLProps, ReactNode } from "react";
import { PropsWithSpread } from "types";

export type NavLink = PropsWithSpread<
{
/**
* Whether this nav item is currently selected.
*/
isSelected?: boolean;
/**
* The label of the link.
*/
label: ReactNode;
/**
* The URL of the link.
*/
url?: string;
export type NavLinkBase = {
/**
* Whether this nav item is currently selected.
*/
isSelected?: boolean;
/**
* The label of the link.
*/
label: ReactNode;
/**
* The URL of the link.
*/
url?: string;
};

export type NavLinkAnchor = PropsWithSpread<
NavLinkBase & {
url: string;
},
HTMLProps<HTMLAnchorElement>
HTMLAttributes<HTMLAnchorElement>
>;

export type NavLinkButton = PropsWithSpread<
NavLinkBase & {
url?: never;
},
HTMLAttributes<HTMLButtonElement>
>;

export type NavLink = NavLinkAnchor | NavLinkButton;

export type NavMenu = {
/**
* Whether to align the dropdown to the right edge of the navigation item.
Expand Down
13 changes: 13 additions & 0 deletions src/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* The Vanilla theme types.
*/
export enum Theme {
/**
* The dark Vanilla theme.
*/
DARK = "dark",
/**
* The light Vanilla theme.
*/
LIGHT = "light",
}
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export type {
NavigationProps,
NavItem,
NavLink,
NavLinkAnchor,
NavLinkBase,
NavLinkButton,
} from "./components/Navigation";
export type { NotificationProps } from "./components/Notification";
export type { PaginationProps } from "./components/Pagination";
Expand Down Expand Up @@ -125,13 +128,15 @@ export {
} from "hooks";
export type { WindowFitment } from "hooks";

export { isNavigationAnchor, isNavigationButton } from "utils";

export type {
ClassName,
Headings,
PropsWithSpread,
SortDirection,
SubComponentProps,
Theme,
TSFixMe,
ValueOf,
} from "./types";
export { Theme } from "./enums";
14 changes: 0 additions & 14 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,3 @@ export type TSFixMe = any; // eslint-disable-line @typescript-eslint/no-explicit
* defined in EnumLike.
*/
export type ValueOf<T> = T[keyof T];

/**
* The Vanilla theme types.
*/
export enum Theme {
/**
* The dark Vanilla theme.
*/
DARK = "dark",
/**
* The light Vanilla theme.
*/
LIGHT = "light",
}
16 changes: 16 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NavLink, NavLinkAnchor, NavLinkButton } from "components/Navigation";

export const IS_DEV = process.env.NODE_ENV === "development";

/**
Expand Down Expand Up @@ -27,3 +29,17 @@ export const highlightSubString = (
match: newStr !== str,
};
};

/**
* Whether a navigation item is an anchor.
* @param link - The navigation item.
*/
export const isNavigationAnchor = (link: NavLink): link is NavLinkAnchor =>
!!link.url;

/**
* Whether a navigation item is a button.
* @param link - The navigation item.
*/
export const isNavigationButton = (link: NavLink): link is NavLinkButton =>
!link.url;

0 comments on commit 45b0e9e

Please sign in to comment.