diff --git a/cypress/components/global-header/global-header.cy.tsx b/cypress/components/global-header/global-header.cy.tsx index 576a7c0a34..7f9ee0a3a0 100644 --- a/cypress/components/global-header/global-header.cy.tsx +++ b/cypress/components/global-header/global-header.cy.tsx @@ -1,7 +1,10 @@ /* eslint-disable jest/valid-expect, jest/valid-expect-in-promise */ import React from "react"; import GlobalHeader from "../../../src/components/global-header"; -import { FullMenuExample } from "../../../src/components/global-header/global-header-test.stories"; +import { + FullMenuExample, + GlobalHeaderWithErrorHandler, +} from "../../../src/components/global-header/global-header-test.stories"; import CypressMountWithProviders from "../../support/component-helper/cypress-mount"; import carbonLogo from "../../../logo/carbon-logo.png"; @@ -9,6 +12,12 @@ import navigationBar from "../../locators/navigation-bar"; import { globalHeader, globalHeaderLogo } from "../../locators/global-header"; context("Testing Global Header component", () => { + it("should not cause a ResizeObserver-related error to occur", () => { + CypressMountWithProviders(); + cy.wait(500); + cy.get("#error-div").should("have.text", ""); + }); + it("should check that z-index of component is greater than that of NavigationBar", () => { CypressMountWithProviders(); globalHeader().invoke("css", "zIndex").as("globalHeaderZIndex"); diff --git a/cypress/components/menu/menu.cy.tsx b/cypress/components/menu/menu.cy.tsx index dd9854e6ca..134a3394d8 100644 --- a/cypress/components/menu/menu.cy.tsx +++ b/cypress/components/menu/menu.cy.tsx @@ -63,6 +63,7 @@ import { MenuDividerComponent, InGlobalHeaderStory, } from "../../../src/components/menu/menu-test.stories"; +import { NavigationBarWithSubmenuAndChangingHeight } from "../../../src/components/navigation-bar/navigation-bar-test.stories"; const span = "span"; const div = "div"; @@ -1960,7 +1961,7 @@ context("Testing Menu component", () => { }); }); - describe("when inside a GlobalHeader", () => { + describe("when inside a Navigation Bar", () => { it("all the content of a long submenu can be accessed with the keyboard while remaining visible", () => { CypressMountWithProviders(); @@ -1978,5 +1979,32 @@ context("Testing Menu component", () => { '[data-component="submenu-wrapper"] ul > li:nth-child(20)' ); }); + + it("all the content of a long submenu can be accessed with the keyboard while remaining visible if the navbar height changes", () => { + CypressMountWithProviders(); + + cy.viewport(1000, 500); + + menuComponent(1).trigger("keydown", keyCode("downarrow")); + submenuItem(1).should("have.length", 21); + + // navigate to "change height" item and press it + for (let i = 0; i < 3; i++) { + cy.focused().trigger("keydown", keyCode("downarrow")); + } + cy.focused().trigger("keydown", keyCode("Enter")); + + // reopen menu and scroll to bottom with keyboard + cy.wait(100); + menuComponent(1).trigger("keydown", keyCode("downarrow")); + + for (let i = 0; i < 21; i++) { + cy.focused().trigger("keydown", keyCode("downarrow")); + } + + cy.checkInViewport( + '[data-component="submenu-wrapper"] ul > li:nth-child(21)' + ); + }); }); }); diff --git a/src/components/global-header/global-header-test.stories.tsx b/src/components/global-header/global-header-test.stories.tsx index a684e6955b..bcff7fd3a5 100644 --- a/src/components/global-header/global-header-test.stories.tsx +++ b/src/components/global-header/global-header-test.stories.tsx @@ -1,7 +1,7 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { ComponentMeta, ComponentStory } from "@storybook/react"; -import GlobalHeader from "./global-header.component"; +import GlobalHeader, { GlobalHeaderProps } from "./global-header.component"; import { Menu, MenuItem, MenuDivider } from "../menu"; import VerticalDivider from "../vertical-divider"; import NavigationBar from "../navigation-bar"; @@ -96,3 +96,23 @@ export const FullMenuExample = () => ( ); + +export const GlobalHeaderWithErrorHandler = ({ + ...props +}: GlobalHeaderProps) => { + const [error, setError] = useState(""); + useEffect(() => { + const handleError = (e: ErrorEvent) => { + setError(e.message); + }; + window.addEventListener("error", handleError); + + return () => window.removeEventListener("error", handleError); + }); + return ( + <> + +
{error}
+ + ); +}; diff --git a/src/components/global-header/global-header.spec.tsx b/src/components/global-header/global-header.spec.tsx index 060465d45f..6b5e2839e7 100644 --- a/src/components/global-header/global-header.spec.tsx +++ b/src/components/global-header/global-header.spec.tsx @@ -1,24 +1,12 @@ import { render, screen } from "@testing-library/react"; import React from "react"; import GlobalHeader, { GlobalHeaderProps } from "./global-header.component"; -import Logger from "../../__internal__/utils/logger"; - -// mock Logger.deprecate so that no console warnings occur while running the tests -const loggerSpy = jest.spyOn(Logger, "deprecate"); function renderer(props?: GlobalHeaderProps) { return render(foobar); } describe("Global Header", () => { - beforeAll(() => { - loggerSpy.mockImplementation(() => {}); - }); - - afterAll(() => { - loggerSpy.mockRestore(); - }); - it("should be visible with correct accessible name", () => { renderer(); expect(screen.getByRole("navigation")).toHaveAccessibleName( diff --git a/src/components/navigation-bar/components.test-pw.tsx b/src/components/navigation-bar/components.test-pw.tsx index e8aa9dd791..505a54f963 100644 --- a/src/components/navigation-bar/components.test-pw.tsx +++ b/src/components/navigation-bar/components.test-pw.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { ComponentStory } from "@storybook/react"; import NavigationBar, { NavigationBarProps } from "."; import { Menu, MenuDivider, MenuItem } from "../menu"; @@ -218,3 +218,23 @@ Fixed.parameters = { docs: { inlineStories: false, iframeHeight: 200 }, themeProvider: { chromatic: { theme: "sage" } }, }; + +export const NavigationBarWithErrorHandler = ({ + ...props +}: NavigationBarProps) => { + const [error, setError] = useState(""); + useEffect(() => { + const handleError = (e: ErrorEvent) => { + setError(e.message); + }; + window.addEventListener("error", handleError); + + return () => window.removeEventListener("error", handleError); + }); + return ( + <> + +
{error}
+ + ); +}; diff --git a/src/components/navigation-bar/fixed-navigation-bar-context.spec.tsx b/src/components/navigation-bar/fixed-navigation-bar-context.spec.tsx index 78842c449d..5ae4e9194b 100644 --- a/src/components/navigation-bar/fixed-navigation-bar-context.spec.tsx +++ b/src/components/navigation-bar/fixed-navigation-bar-context.spec.tsx @@ -14,15 +14,13 @@ const ConsumerComponent = () => { }; const mockNavbarElement = { offsetHeight: 40 } as HTMLElement; +const navbarRef = { current: mockNavbarElement }; const MockComponent = ( - props: Omit + props: Omit ) => { return ( - + ); diff --git a/src/components/navigation-bar/fixed-navigation-bar.context.tsx b/src/components/navigation-bar/fixed-navigation-bar.context.tsx index 6f62931f00..d47c3d60b9 100644 --- a/src/components/navigation-bar/fixed-navigation-bar.context.tsx +++ b/src/components/navigation-bar/fixed-navigation-bar.context.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useState, useCallback } from "react"; +import React, { createContext, useState, useCallback, useEffect } from "react"; import useResizeObserver from "../../hooks/__internal__/useResizeObserver/useResizeObserver"; import { NavigationBarProps } from "."; @@ -15,7 +15,7 @@ export interface FixedNavigationBarContextProviderProps NavigationBarProps, "position" | "orientation" | "offset" | "children" > { - navbarElement: HTMLElement | null; + navbarRef: React.RefObject; } export const FixedNavigationBarContextProvider = ({ @@ -23,16 +23,22 @@ export const FixedNavigationBarContextProvider = ({ orientation, offset, children, - navbarElement, + navbarRef, }: FixedNavigationBarContextProviderProps) => { - const [navbarHeight, setNavbarHeight] = useState(navbarElement?.offsetHeight); + const [navbarHeight, setNavbarHeight] = useState( + navbarRef.current?.offsetHeight + ); const updateHeight = useCallback( - () => setNavbarHeight(navbarElement?.offsetHeight), - [navbarElement] + () => setNavbarHeight(navbarRef.current?.offsetHeight), + [navbarRef] ); - useResizeObserver({ current: navbarElement }, updateHeight); + useEffect(() => { + updateHeight(); + }, [updateHeight]); + + useResizeObserver(navbarRef, updateHeight); let submenuMaxHeight; diff --git a/src/components/navigation-bar/navigation-bar-test.stories.tsx b/src/components/navigation-bar/navigation-bar-test.stories.tsx index 4060dfb53e..912e043816 100644 --- a/src/components/navigation-bar/navigation-bar-test.stories.tsx +++ b/src/components/navigation-bar/navigation-bar-test.stories.tsx @@ -1,9 +1,10 @@ -import React from "react"; +import React, { useRef } from "react"; import NavigationBar, { NavigationBarProps } from "."; +import { Menu, MenuItem } from "../menu"; export default { title: "Navigation Bar/Test", - includeStories: ["DefaultStory"], + includeStories: ["DefaultStory", "NavigationBarWithSubmenuAndChangingHeight"], parameters: { info: { disable: true }, chromatic: { @@ -25,3 +26,45 @@ DefaultStory.args = { position: undefined, offset: "0", }; + +export const NavigationBarWithSubmenuAndChangingHeight = () => { + const wrapperRef = useRef(null); + const toggleHeight = () => { + const navbarElement = wrapperRef.current?.querySelector("nav"); + if (navbarElement) { + navbarElement.style.height = + navbarElement.style.height === "100px" ? "40px" : "100px"; + } + }; + return ( +
+ + + + {}}>Foo 1 + {}}>Foo 2 + {}}>Foo 3 + Change Height! + {}}>Foo 4 + {}}>Foo 5 + {}}>Foo 6 + {}}>Foo 7 + {}}>Foo 8 + {}}>Foo 9 + {}}>Foo 10 + {}}>Foo 11 + {}}>Foo 12 + {}}>Foo 13 + {}}>Foo 14 + {}}>Foo 15 + {}}>Foo 16 + {}}>Foo 17 + {}}>Foo 18 + {}}>Foo 19 + {}}>Foo 20 + + + +
+ ); +}; diff --git a/src/components/navigation-bar/navigation-bar.component.tsx b/src/components/navigation-bar/navigation-bar.component.tsx index 86397ce734..9a795441cc 100644 --- a/src/components/navigation-bar/navigation-bar.component.tsx +++ b/src/components/navigation-bar/navigation-bar.component.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useRef } from "react"; import { PaddingProps, FlexboxProps } from "styled-system"; import StyledNavigationBar from "./navigation-bar.style"; import { FixedNavigationBarContextProvider } from "./fixed-navigation-bar.context"; @@ -32,35 +32,35 @@ export const NavigationBar = ({ children, ariaLabel, position, - offset = "0", + offset = "0px", orientation, isGlobal, ...props }: NavigationBarProps): JSX.Element => { - const [navbarElement, setNavbarElement] = useState(null); + const navbarRef = useRef(null); return ( - - {!isLoading && children} - - + +
); }; diff --git a/src/components/navigation-bar/navigation-bar.pw.tsx b/src/components/navigation-bar/navigation-bar.pw.tsx index 65c7513314..7724f16a68 100644 --- a/src/components/navigation-bar/navigation-bar.pw.tsx +++ b/src/components/navigation-bar/navigation-bar.pw.tsx @@ -15,6 +15,7 @@ import { ContentMaxWidthBox, Sticky, Fixed, + NavigationBarWithErrorHandler, } from "./components.test-pw"; import navigationBar from "../../../playwright/components/navigation-bar"; @@ -32,6 +33,15 @@ const variants = [ const offsetVal = [25, 100, -100]; test.describe("Test props for NavigationBar component", () => { + test("should not cause a ResizeObserver-related error to occur", async ({ + mount, + page, + }) => { + await mount(); + + await expect(page.locator("#error-div")).toContainText(""); + }); + specialCharacters.forEach((childrenValue) => { test(`should render with ${childrenValue} as a children`, async ({ mount, diff --git a/src/components/navigation-bar/navigation-bar.spec.tsx b/src/components/navigation-bar/navigation-bar.spec.tsx index 79fcbc7243..02fd159dd5 100644 --- a/src/components/navigation-bar/navigation-bar.spec.tsx +++ b/src/components/navigation-bar/navigation-bar.spec.tsx @@ -52,7 +52,9 @@ describe("NavigationBar", () => {
); - expect(wrapper.prop("data-component")).toBe("navigation-bar"); + expect(wrapper.find(StyledNavigationBar).prop("data-component")).toBe( + "navigation-bar" + ); }); it("should provide ariaLabel correctly", () => { @@ -62,7 +64,9 @@ describe("NavigationBar", () => { ); - expect(wrapper.prop("aria-label")).toBe("my aria label"); + expect(wrapper.find(StyledNavigationBar).prop("aria-label")).toBe( + "my aria label" + ); }); it("should render `light` scheme as default", () => { @@ -72,7 +76,9 @@ describe("NavigationBar", () => { ); - expect(wrapper.props().navigationType).toBe("light"); + expect(wrapper.find(StyledNavigationBar).props().navigationType).toBe( + "light" + ); }); it("should render correct styles in `light` scheme", () => { @@ -190,7 +196,7 @@ describe("NavigationBar", () => { assertStyleMatch( { position: `${position}`, - [orientation]: offset || "0", + [orientation]: offset || "0px", ...(position === "fixed" && { width: "100%", boxSizing: "border-box",