From fa46ee63268e78971046bc58f702f209bf1cd4d1 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 18 Dec 2023 15:26:56 +0200 Subject: [PATCH] (HDS-1975) Adjust to new animation and render style Another PR made the mobileMenu element always visible and animates it vertically. --- .../HeaderActionBarNavigationMenu.module.scss | 4 +- .../HeaderActionBarNavigationMenu.test.tsx | 57 ++++++++++++------- .../HeaderActionBarNavigationMenu.tsx | 34 ++++++++--- ...eaderActionBarNavigationMenu.test.tsx.snap | 1 + 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.module.scss b/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.module.scss index 315732c20b0..84833f0cd1f 100644 --- a/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.module.scss +++ b/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.module.scss @@ -45,7 +45,9 @@ overflow: hidden; position: absolute; transform: translateY(-100%) translateY(1px); - transition: var(--animation-duration-dropwdown) transform var(--animation-close-delay-dropdown), var(--animation-duration-dropwdown) min-height calc(var(--animation-duration-dropwdown) + var(--animation-close-delay-dropdown)); + transition: var(--animation-duration-dropwdown) transform var(--animation-close-delay-dropdown), + var(--animation-duration-dropwdown) min-height + calc(var(--animation-duration-dropwdown) + var(--animation-close-delay-dropdown)); width: 100%; } diff --git a/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.test.tsx b/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.test.tsx index f86e69770bc..ab7f5d394f6 100644 --- a/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.test.tsx +++ b/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.test.tsx @@ -25,6 +25,7 @@ type TestTools = RenderResult & { closeMobileMenu: () => Promise; findVisibleSectionLinksByMenuIds: (menuIds: string[]) => Promise; getNavSections: () => ReturnType; + getCSSVisibleSections: () => HTMLElement[]; getActiveLink: () => HTMLAnchorElement; getPreviousLink: () => HTMLAnchorElement | null; selectMenuItem: (menuItem: MenuItem) => Promise; @@ -176,21 +177,29 @@ const renderHeader = (): TestTools => { const result = render(HeaderWithMenus()); const { container, getByText, getAllByText } = result; + const getSections = () => container.querySelectorAll('section'); const getNavSections: TestTools['getNavSections'] = () => container.querySelectorAll('section > nav'); const isElementAriaHidden = (el: Element) => el.getAttribute('aria-hidden') === 'true'; const isElementAriaVisible = (el: Element) => !isElementAriaHidden(el); const getDropdownButtonForLink = (el: Element) => el.parentElement?.querySelector('button') as HTMLButtonElement; - const getVisibleNav = () => { + const getAriaVisibleNav = () => { return Array.from(getNavSections()).find((el) => isElementAriaVisible(el.parentElement as HTMLElement), ) as HTMLElement; }; + + const getCSSVisibleSections = () => { + return Array.from(getSections()).filter((el) => { + const classes = String(el.getAttribute('class')); + return !classes.includes('hidden'); + }) as HTMLElement[]; + }; const toggleMobileMenu = async (shouldBeOpen: boolean) => { const menuButton = getByText('Menu') as HTMLButtonElement; fireEvent.click(menuButton); await waitFor(() => { - const isClosed = getNavSections().length === 0; + const isClosed = getCSSVisibleSections().length === 0; if (isClosed === shouldBeOpen) { throw new Error('Navigation element mismatch'); } @@ -206,7 +215,7 @@ const renderHeader = (): TestTools => { }; const findVisibleSectionLinksByMenuIds: TestTools['findVisibleSectionLinksByMenuIds'] = async (menuIds) => { - const visibleNav = getVisibleNav(); + const visibleNav = getAriaVisibleNav(); // ignore links listed in activeListItem. They are invisible. const activeListItem = visibleNav.querySelector('li.activeListItem'); return waitFor(() => { @@ -223,12 +232,12 @@ const renderHeader = (): TestTools => { }; const getActiveLink: TestTools['getActiveLink'] = () => { - const visibleNav = getVisibleNav(); + const visibleNav = getAriaVisibleNav(); return visibleNav.querySelector('a.activeMobileLink') as HTMLAnchorElement; }; const getPreviousLink: TestTools['getPreviousLink'] = () => { - const visibleNav = getVisibleNav(); + const visibleNav = getAriaVisibleNav(); return visibleNav.querySelector('span.previousMobileLink') as HTMLAnchorElement; }; @@ -241,7 +250,7 @@ const renderHeader = (): TestTools => { return Promise.reject(new Error(`Menu item ${item.id} not found`)); } const getCurrentNavIndex = () => { - const currentNav = getVisibleNav(); + const currentNav = getAriaVisibleNav(); const index = Array.from(getNavSections()).indexOf(currentNav); if (index === -1) { throw new Error('getCurrentNavIndex is -1'); @@ -328,6 +337,7 @@ const renderHeader = (): TestTools => { verifyPreviousItem, navigateTo, navigateBack, + getCSSVisibleSections, }; }; @@ -370,11 +380,21 @@ describe(' spec', () => { await findVisibleSectionLinksByMenuIds(menus.map((item) => item.id)); expect(getNavSections()).toHaveLength(1); }); - it('Nav sections are rendered only when needed and removed when not needed', async () => { - const { openMobileMenu, navigateTo, getNavSections, navigateBack, closeMobileMenu } = renderHeader(); - expect(getNavSections()).toHaveLength(0); - await openMobileMenu(); + it('Nav sections are rendered and visible only when needed and removed when not needed', async () => { + const { + openMobileMenu, + navigateTo, + getNavSections, + navigateBack, + closeMobileMenu, + getCSSVisibleSections, + } = renderHeader(); + // one is always rendered expect(getNavSections()).toHaveLength(1); + // but it is hidden + expect(getCSSVisibleSections()).toHaveLength(0); + await openMobileMenu(); + expect(getCSSVisibleSections()).toHaveLength(1); await navigateTo(getMenuItem([0])); expect(getNavSections()).toHaveLength(2); await navigateTo(getMenuItem([0, 2])); @@ -384,23 +404,24 @@ describe(' spec', () => { await navigateBack(frontPageLabel); expect(getNavSections()).toHaveLength(1); await closeMobileMenu(); - expect(getNavSections()).toHaveLength(0); + expect(getCSSVisibleSections()).toHaveLength(0); + expect(getNavSections()).toHaveLength(1); }); it('Previous and active links change while navigating', async () => { const { openMobileMenu, navigateTo, - getNavSections, navigateBack, verifyActiveItem, verifyPreviousItem, + getCSSVisibleSections, } = renderHeader(); const menu3 = getMenuItem([3]); const menu31 = getMenuItem([3, 1]); const menu32 = getMenuItem([3, 2]); const menu0 = getMenuItem([0]); const menu02 = getMenuItem([0, 2]); - expect(getNavSections()).toHaveLength(0); + expect(getCSSVisibleSections()).toHaveLength(0); await openMobileMenu(); expect(verifyActiveItem(frontPageLabel)).toBeTruthy(); expect(verifyPreviousItem(null)).toBeTruthy(); @@ -440,8 +461,7 @@ describe(' spec', () => { expect(verifyPreviousItem(null)).toBeTruthy(); }); it('If the top level active link is clicked, menu is closed.', async () => { - const { openMobileMenu, getNavSections, getActiveLink } = renderHeader(); - expect(getNavSections()).toHaveLength(0); + const { openMobileMenu, getActiveLink, getCSSVisibleSections } = renderHeader(); await openMobileMenu(); const activeLinkToFrontpage = getActiveLink(); @@ -449,12 +469,11 @@ describe(' spec', () => { activeLinkToFrontpage.setAttribute('href', ''); fireEvent.click(activeLinkToFrontpage); await waitFor(() => { - expect(getNavSections()).toHaveLength(0); + expect(getCSSVisibleSections()).toHaveLength(0); }); }); it('If a lower level active link is clicked, menu is closed and onClick handler is called.', async () => { - const { openMobileMenu, getNavSections, getActiveLink, selectMenuItem } = renderHeader(); - expect(getNavSections()).toHaveLength(0); + const { openMobileMenu, getActiveLink, selectMenuItem, getCSSVisibleSections } = renderHeader(); await openMobileMenu(); await selectMenuItem(getMenuItem([0])); @@ -462,7 +481,7 @@ describe(' spec', () => { fireEvent.click(activeLinkToFrontpage); await waitFor(() => { expect(onClickTracker).toHaveBeenCalledTimes(1); - expect(getNavSections()).toHaveLength(0); + expect(getCSSVisibleSections()).toHaveLength(0); }); }); }); diff --git a/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.tsx b/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.tsx index 460f9da5103..603d28259b0 100644 --- a/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.tsx +++ b/packages/react/src/components/header/components/headerActionBar/HeaderActionBarNavigationMenu.tsx @@ -16,6 +16,7 @@ import { HeaderLink } from '../headerLink'; import { IconAngleLeft } from '../../../../icons'; import { LinkProps } from '../../../../internal/LinkItem'; import HeaderActionBarLogo from './HeaderActionBarLogo'; +import useIsomorphicLayoutEffect from '../../../../hooks/useIsomorphicLayoutEffect'; type NavigationSectionType = { logo: React.ReactNode; @@ -239,7 +240,7 @@ export const HeaderActionBarNavigationMenu = ({ const goToPreviousMenuLevel = () => { const last = selectedMenuLevels.pop(); - // Last item is removed after animation is done. + // Last item is removed after horizontal animation is done. // It is marked as inactive here and removed in resetMenusAfterAnimation() setSelectedMenuLevels([ ...selectedMenuLevels.map((item) => { @@ -338,8 +339,6 @@ export const HeaderActionBarNavigationMenu = ({ } }); - if (!mobileMenuOpen) return null; - const goDeeper = (link: React.ReactElement) => { if (isAnimating()) { return; @@ -358,17 +357,34 @@ export const HeaderActionBarNavigationMenu = ({ setMobileMenuOpen(false); }; + const setCurrentMenuHeight = () => { + const containerElement = navContainerRef.current; + if (!containerElement) { + return; + } + // Set the height of the menu container + const renderedChildIndex = resetMenusAfterAnimation() - 1; + const currentMenuSection = containerElement.children[renderedChildIndex]; + if (!currentMenuSection) { + return; + } + navContainerRef.current.style.height = `${currentMenuSection.clientHeight}px`; + }; + const menuSectionsAnimationDone = async (e: TransitionEvent) => { const targetElement = e.target as HTMLElement; if (e.propertyName === 'transform' && targetElement.firstChild.nodeName === 'SECTION') { shouldSetFocus.current = true; - // Set the height of the menu container - const renderedChildIndex = resetMenusAfterAnimation() - 1; - const currentMenuSection = targetElement.children[renderedChildIndex]; - navContainerRef.current.style.height = `${currentMenuSection.clientHeight}px`; + setCurrentMenuHeight(); } }; + useIsomorphicLayoutEffect(() => { + if (mobileMenuOpen) { + setCurrentMenuHeight(); + } + }, [mobileMenuOpen]); + const RenderNavigationSection = ({ links, activeLink, @@ -432,8 +448,8 @@ export const HeaderActionBarNavigationMenu = ({ return ( spec renders the component and menu c >