-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1703 from NYPL/DSD-1872/temp-header
DSD-1872: temp Header
- Loading branch information
Showing
51 changed files
with
5,109 additions
and
235 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 />, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.