Skip to content

Commit

Permalink
Merge pull request #1703 from NYPL/DSD-1872/temp-header
Browse files Browse the repository at this point in the history
DSD-1872: temp Header
  • Loading branch information
EdwinGuzman authored Nov 18, 2024
2 parents 8ddb608 + 3b4df2c commit f5db208
Show file tree
Hide file tree
Showing 51 changed files with 5,109 additions and 235 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Currently, this repo is in Prerelease. When it is released, this project will ad

## Prerelease

### Adds

- Adds the NYPL Header as a DS component as a temporary solution for a specific application. It's only meant to be used internally and teams are still encouraged to use the `nypl-header-app`.

## 3.4.2 (November 7, 2024)

### Adds
Expand Down
473 changes: 239 additions & 234 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nypl/design-system-react-components",
"version": "3.4.2",
"version": "3.4.3-rc9",
"description": "NYPL Reservoir Design System React Components",
"repository": {
"type": "git",
Expand Down Expand Up @@ -113,6 +113,7 @@
"jest-axe": "9.0.0",
"jest-environment-jsdom": "29.7.0",
"jest-transformer-svg": "2.0.2",
"js-cookie": "^3.0.5",
"lint-staged": "10.5.4",
"normalize.css": "8.0.1",
"prettier": "2.4.1",
Expand Down
17 changes: 17 additions & 0 deletions src/components/Header/Header.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ArgTypes, Canvas, Description, Meta, Source } from "@storybook/blocks";

import * as HeaderStories from "./Header.stories.tsx";

<Meta of={HeaderStories} />

# Header

## Overview

Note: This is a temporary addition to Reservoir and should not be used in
production unless told otherwise. Continue using the Header from
`ds-header.nypl.org` and follow implementation instructions from the
[nypl-header-app](https://github.com/NYPL/nypl-header-app) repository and the
[Storybook guide](https://nypl.github.io/nypl-design-system/reservoir/v3/?path=/docs/development-guide-header--docs).

<Canvas of={HeaderStories.WithControls} />
18 changes: 18 additions & 0 deletions src/components/Header/Header.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Meta, StoryObj } from "@storybook/react";

import Header from "./Header";

const meta: Meta<typeof Header> = {
title: "Components/Temp/Header",
component: Header,
argTypes: {},
};

export default meta;
type Story = StoryObj<typeof Header>;

export const WithControls: Story = {
args: {},
parameters: {},
render: () => <Header />,
};
152 changes: 152 additions & 0 deletions src/components/Header/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe } from "jest-axe";
import * as renderer from "react-test-renderer";

import Header from "./Header";
import { mockLoginCookie } from "./utils/authApiMockResponse";
import { refineryResponse } from "./utils/sitewideAlertsMocks";

jest.mock("js-cookie", () => ({
get: () => mockLoginCookie,
remove: jest.fn(),
}));

describe("Header Accessibility", () => {
it("passes axe accessibility test", async () => {
// Mock the fetch API call in `SitewideAlerts`.
(global as any).fetch = jest.fn(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve(refineryResponse),
})
) as jest.Mock;

const { container } = await waitFor(() =>
render(<Header isProduction={false} />)
);
expect(await axe(container)).toHaveNoViolations();

jest.clearAllMocks();
});
});

// TODO: These tests do not currently test the mobile web view.
// We need to determine a way of doing this for all responsive
// components, and will add this in at a later date.
describe.skip("Header", () => {
beforeEach(async () => {
// Mock the fetch API call in `SitewideAlerts`.
(global as any).fetch = jest.fn(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve(refineryResponse),
})
) as jest.Mock;

// Mocks matchMedia so that the desktop view renders rather than mobile.
global.matchMedia = jest.fn(() => ({
addListener: () => {},
removeListener: () => {},
matches: true,
})) as jest.Mock;
});

afterAll(() => {
jest.clearAllMocks();
});

it("renders a skip navigation", () => {
const skipNavigation = screen.getAllByRole("navigation")[0];
const { getByRole } = within(skipNavigation);

expect(skipNavigation).toBeInTheDocument();
expect(skipNavigation).toHaveAttribute("aria-label", "Skip Navigation");
expect(getByRole("list")).toBeInTheDocument();
});

it("renders a notification", () => {
const notification = screen.getByRole("complementary");
const { getByText } = within(notification);

expect(notification).toBeInTheDocument();
expect(getByText(/masks are encouraged/i)).toBeInTheDocument();
});

it("renders the NYPL logo", () => {
const nyplLink = screen.getAllByRole("link", {
name: "The New York Public Library",
})[0];

const logo = within(nyplLink).getByRole("img");

expect(logo).toHaveAttribute("aria-label", "NYPL Header Logo");
});

it("renders the upper links", () => {
// Removes automatically added, unused Chakra toast elements.
document.getElementById("chakra-toast-portal")?.remove();

// The first list is the skip navigation.
// The second list is the list of alerts in the `SitewideAlerts` component.
// The third list is the upper navigation.

const upperList = screen.getAllByRole("list")[2];
const upperLinks = within(upperList).getAllByRole("listitem");

expect(upperLinks.length).toEqual(6);
expect(upperLinks[0]).toHaveTextContent(/log in/i);
expect(upperLinks[5]).toHaveTextContent(/shop/i);
});

it("renders the lower links", () => {
// Removes automatically added, unused Chakra toast elements.
document.getElementById("chakra-toast-portal")?.remove();

// The first list is the skip navigation.
// The second list is the list of alerts in the `SitewideAlerts` component.
// The third list is the upper navigation.
// The fourth list is the lower navigation.
const lowerList = screen.getAllByRole("list")[3];
const lowerLinks = within(lowerList).getAllByRole("listitem");

expect(lowerLinks.length).toEqual(8);
expect(lowerLinks[0]).toHaveTextContent(/books/i);
expect(lowerLinks[7]).toHaveTextContent(/search/i);
});

it("opens the login menu", () => {
// Removes automatically added, unused Chakra toast elements.
document.getElementById("chakra-toast-portal")?.remove();

// The third list is the upper navigation links.
let upperList = screen.getAllByRole("list")[2];
let upperLinks = within(upperList).getAllByRole("listitem");

expect(upperLinks.length).toEqual(6);

const myAccountButton = within(upperLinks[0]).getByRole("button");
expect(upperLinks[0]).toHaveTextContent(/my account/i);
expect(upperLinks[1]).toHaveTextContent(/locations/i);

userEvent.click(myAccountButton);

upperList = screen.getAllByRole("list")[2];
upperLinks = within(upperList).getAllByRole("listitem");

// Login menu opens, revealing two additional list items.
expect(upperLinks.length).toEqual(8);
expect(upperLinks[0]).toHaveTextContent(/close/i);
expect(upperLinks[1]).toHaveTextContent(/go to the catalog/i);
});

it("renders the horizontal rule", () => {
expect(screen.getByRole("separator")).toBeInTheDocument();
});

it("renders the UI snapshot correctly", () => {
const header = renderer.create(<Header isProduction={false} />).toJSON();

expect(header).toMatchSnapshot();
});
});
145 changes: 145 additions & 0 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
chakra,
Box,
HStack,
Spacer,
useMediaQuery,
useMultiStyleConfig,
VStack,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";

/** Internal Header-only components */
import HeaderLowerNav from "./components/HeaderLowerNav";
import HeaderMobileIconNav from "./components/HeaderMobileIconNav";
import HeaderSitewideAlerts from "./components/HeaderSitewideAlerts";
import HeaderUpperNav from "./components/HeaderUpperNav";
/** Internal Header-only utils */
import { HeaderProvider } from "./context/headerContext";
import EncoreCatalogLogOutTimer from "./utils/encoreCatalogLogOutTimer";

// import useNYPLBreakpoints from "../../hooks/useNYPLBreakpoints";
import SkipNavigation from "../SkipNavigation/SkipNavigation";
import Link from "../Link/Link";
import Logo from "../Logo/Logo";
import HorizontalRule from "../HorizontalRule/HorizontalRule";
import { headerBreakpoints } from "../../theme/foundations/breakpoints";

export interface HeaderProps {
/** Whether to render sitewide alerts or not. True by default. */
fetchSitewideAlerts?: boolean;
/** Whether or not the `Header` is in production mode. True by default. */
isProduction?: boolean;
}

/**
* The NYPL `Header` component is the top-level component of the site. It
* contains features for logging in, logging out, searching, and navigating
* the NYPL.org site.
*/
export const Header = chakra(
({ fetchSitewideAlerts = true, isProduction = true }: HeaderProps) => {
// isLargerThanLarge is greater than 960px
// const { isLargerThanLarge } = useNYPLBreakpoints();
// The Header's "mobile" is 832px and below.
const [isLargerThanMobile] = useMediaQuery([
`(min-width: ${headerBreakpoints.mh})`,
]);
const [isLoaded, setIsLoaded] = useState(false);

const styles = useMultiStyleConfig("Header", {});
// Create a new instance of the EncoreCatalogLogOutTimer. The timer will
// start when the component is mounted. Even though the patron's information
// is no longer displayed in the header, we still want to make sure that
// they are logged out in various NYPL sites.
const encoreCatalogLogOutTimer = new EncoreCatalogLogOutTimer(
Date.now(),
false
);

// Once the `Header` component is mounted, start a timer that will
// log the user out of Vega and the NYPL Catalog after 30 minutes.
useEffect(() => {
encoreCatalogLogOutTimer.setEncoreLoggedInTimer(window.location.host);
});

// We also want to delete a certain log in-related cookie but should
// actively check and delete it every 3 seconds.
useEffect(() => {
const interval = setInterval(() => {
encoreCatalogLogOutTimer.removeLoggedInCookie();
}, 3000);
return () => clearInterval(interval);
});

useEffect(() => {
setIsLoaded(true);
}, []);

return (
<HeaderProvider isProduction={isProduction}>
<Box
__css={{
...styles,
// For Vega override, this is the browser's default.
"& > nav li": { marginBottom: "0 !important" },
"& svg": { verticalAlign: "baseline !important" },
"& fieldset": {
marginBottom: "0px !important",
},
"& input": {
marginBottom: "0px !important",
},
}}
>
<SkipNavigation />
{fetchSitewideAlerts ? <HeaderSitewideAlerts /> : null}
<header>
<HStack __css={styles.container}>
<Link
aria-label="The New York Public Library"
href="https://nypl.org"
__css={styles.logo}
>
{!isLoaded ? (
<Logo
key="logo-1"
aria-label="NYPL Header Logo"
name={"nyplFullBlack"}
title="NYPL Header Logo"
/>
) : (
<Logo
key="logo-2"
aria-label="NYPL Header Logo"
name={
isLargerThanMobile ? "nyplFullBlack" : "nyplLionBlack"
}
title="NYPL Header Logo"
/>
)}
</Link>
<Spacer />
{!isLoaded ? (
<VStack alignItems="end" sx={styles.navContainer}>
<HeaderUpperNav />
<HeaderLowerNav />
</VStack>
) : isLargerThanMobile ? (
<VStack alignItems="end" sx={styles.navContainer}>
<HeaderUpperNav />
<HeaderLowerNav />
</VStack>
) : (
<HeaderMobileIconNav />
)}
</HStack>
<HorizontalRule __css={styles.horizontalRule} />
</header>
</Box>
</HeaderProvider>
);
}
);

export default Header;
Loading

0 comments on commit f5db208

Please sign in to comment.