From 7cd98b3612f8a0a87ba1787faee9f42da12c17b1 Mon Sep 17 00:00:00 2001 From: Emily Jablonski <65367387+emilyjablonski@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:11:45 -0600 Subject: [PATCH] fix: long links wrapping behind header (#124) --- __tests__/headers/SiteHeader.test.tsx | 2 +- src/headers/SiteHeader.scss | 74 +++++++++++++++--- src/headers/SiteHeader.stories.tsx | 81 +++++++++++++++++++- src/headers/SiteHeader.tsx | 103 +++++++++++++++++--------- 4 files changed, 212 insertions(+), 48 deletions(-) diff --git a/__tests__/headers/SiteHeader.test.tsx b/__tests__/headers/SiteHeader.test.tsx index 7e4b1c8..dafa516 100644 --- a/__tests__/headers/SiteHeader.test.tsx +++ b/__tests__/headers/SiteHeader.test.tsx @@ -54,7 +54,7 @@ describe("", () => { expect(getByText("We're just getting started. We'd love to get your feedback.")).toBeTruthy() expect(getByText("Listings")).toBeTruthy() expect(getByText("Get Assistance")).toBeTruthy() - expect(getByText("My Account")).toBeTruthy() + expect(getByText("Account")).toBeTruthy() expect(queryByText("My Dashboard")).toBeNull() expect(queryByText("My Applications")).toBeNull() expect(queryByText("Account Settings")).toBeNull() diff --git a/src/headers/SiteHeader.scss b/src/headers/SiteHeader.scss index 053695b..a700cb0 100644 --- a/src/headers/SiteHeader.scss +++ b/src/headers/SiteHeader.scss @@ -9,11 +9,14 @@ --base-max-height: 75px; --base-mobile-padding: inherit; --base-mobile-width: 1025px; + --base-desktop-wrap: wrap; + --base-mobile-wrap: initial; --border-color: var(--bloom-color-gray-450); --container-border: none; --notice-text-font-size: var(--bloom-font-size-xs); --notice-display-mobile: block; --notice-display-desktop: block; + --wrapped-background-color: var(--bloom-color-gray-200); // Logo --logo-background-color: var(--bloom-color-white); @@ -58,13 +61,15 @@ --logo-width-slim: 100px; // Link - --link-align-items: flex-end; + --link-align-items-wrapped: center; + --link-align-items-inline: flex-end; --link-bottom-border: 3px solid transparent; --link-font-size: var(--bloom-font-size-xs); --link-font-weight: 400; --link-height: 100%; --link-hover-bottom-border: 3px solid var(--bloom-color-primary); - --link-padding: var(--bloom-s1) var(--bloom-s3) var(--bloom-s4) var(--bloom-s3); + --link-padding-wrapped: var(--bloom-s2) var(--bloom-s3); + --link-padding-inline: var(--bloom-s1) var(--bloom-s3) var(--bloom-s3) var(--bloom-s3); --link-text-color: var(--bloom-color-gray-750); --link-text-hover-color: var(--bloom-color-gray-750); --link-text-desktop-color: var(--bloom-color-gray-750); @@ -73,8 +78,7 @@ // Dropdown --dropdown-container-border-top: none; --dropdown-container-display: inherit; - --dropdown-container-margin: 38.5px 0 0 -127px; - --dropdown-container-width: 143px; + --dropdown-container-top-offset: 75px; --dropdown-item-flex: inherit; --dropdown-item-font-size: var(--bloom-font-size-sm); --mobile-dropdown-item-font-size: var(--bloom-font-size-sm); @@ -92,11 +96,12 @@ --navbar-menu-desktop-margin: inherit; --navbar-menu-desktop-padding: inherit; --navbar-menu-desktop-width: inherit; - --navbar-menu-min-width: 110px; + --navbar-menu-min-width: inherit; --navbar-menu-mobile-height: inherit; --navbar-menu-mobile-margin: inherit; --navbar-menu-mobile-padding: inherit; --navbar-menu-mobile-width: inherit; + --navbar-menu-wrap: inherit; } .site-header__mobile-dropdown-container { @@ -191,6 +196,7 @@ display: flex; flex-direction: row; justify-content: space-between; + flex-wrap: var(--base-mobile-wrap); z-index: 10; max-width: 100%; align-items: var(--base-align-items); @@ -198,11 +204,23 @@ width: var(--base-mobile-width); @media (min-width: $screen-md) { + flex-wrap: var(--base-desktop-wrap); padding: var(--base-desktop-padding); width: var(--base-desktop-width); } } +.site-header__base-wrapped { + margin-top: 0.2rem; + margin-bottom: var(--bloom-s12); + padding-left: 0; +} + +.site-header__base-inline { + margin-top: 0.025rem; + margin-bottom: 0; +} + .site-header__logo-container { display: flex; align-items: center; @@ -212,9 +230,9 @@ padding: var(--logo-container-padding-mobile); @media (min-width: $screen-md) { + flex-grow: 0; padding: var(--logo-container-padding-desktop); flex-shrink: 0; - flex-grow: initial; } } @@ -334,6 +352,8 @@ .site-header__navbar-menu { display: flex; justify-content: flex-end; + flex-basis: inherit; + flex-grow: 0; min-width: var(--navbar-menu-min-width); height: var(--navbar-menu-mobile-height); @@ -342,6 +362,8 @@ padding: var(--navbar-menu-mobile-padding); @media (min-width: $screen-md) { + flex-basis: var(--navbar-menu-wrap); + flex-grow: 1; height: var(--navbar-menu-desktop-height); margin: var(--navbar-menu-desktop-margin); width: var(--navbar-menu-desktop-width); @@ -403,8 +425,33 @@ } } -.site-header__dropdown-title { - flex-shrink: 0; +.site-header__dropdown-title-with-icon { + display: inline-flex; +} + +.site-header__dropdown-title-split { + padding-right: var(--bloom-s1); +} + +.site-header__navbar-wrapped { + background-color: var(--wrapped-background-color); + justify-content: flex-start; + margin-top: var(--bloom-s3); + padding-left: var(--bloom-s2); + .site-header__link { + align-items: var(--link-align-items-wrapped); + padding: var(--link-padding-wrapped); + } +} + +.site-header__navbar-inline { + background-color: var(--background-color); + justify-content: flex-end; + + .site-header__link { + align-items: var(--link-align-items-inline); + padding: var(--link-padding-inline); + } } .site-header__link { @@ -414,9 +461,11 @@ height: var(--link-height); padding: var(--link-padding); display: flex; - align-items: var(--link-align-items); font-weight: var(--link-font-weight); border-bottom: var(--link-bottom-border); + flex-shrink: 1; + text-align: center; + position: relative; @media (min-width: 840px) { padding-right: var(--bloom-s4); @@ -442,10 +491,12 @@ .site-header__dropdown-container { background-color: var(--bloom-color-white); position: absolute; - margin: var(--dropdown-container-margin); + right: 0; + top: var(--dropdown-container-top-offset); border: 1px solid var(--border-color); border-top: var(--dropdown-container-border-top); - width: var(--dropdown-container-width); + min-width: 100%; + width: max-content; display: var(--dropdown-container-display); } @@ -467,6 +518,7 @@ text-align: left; width: 100%; flex: var(--dropdown-item-flex); + word-break: break-word; &:hover, &:focus { diff --git a/src/headers/SiteHeader.stories.tsx b/src/headers/SiteHeader.stories.tsx index a4abf66..bdf6c84 100644 --- a/src/headers/SiteHeader.stories.tsx +++ b/src/headers/SiteHeader.stories.tsx @@ -373,15 +373,19 @@ export const DAHLIAToggleSet = () => ( dropdownItemClassName={"text-2xs"} menuLinks={[ { - title: "Some longer links", + title: "Rent", href: "/", }, { - title: "Some longer links", + title: "Buy", href: "/", }, { - title: "Some longer links", + title: "My Favorites", + href: "/", + }, + { + title: "Get Assistance", href: "/", }, { @@ -416,3 +420,74 @@ export const DAHLIAToggleSet = () => ( ]} /> ) + +export const LongLinks = () => ( + console.log("Clicked English"), active: true }, + { label: "Español", onClick: () => console.log("Clicked Español"), active: false }, + { label: "中文", onClick: () => console.log("Clicked 中文"), active: false }, + ]} + notice="We'd love to get your feedback." + logoWidth={"medium"} + title="Alameda County Housing Portal" + logoSrc="/images/logo_glyph.svg" + menuLinks={[ + { + title: "Excepteur", + href: "/", + }, + { + title: "Voluptate", + href: "/", + }, + { + title: "Reprehenderit", + href: "/", + }, + { + title: "Ut labore et dolore", + subMenuLinks: [ + { + title: "Consectetur adipiscing elit", + href: "/account/dashboard", + }, + { + title: "My Applications", + href: "/account/dashboard", + }, + { + title: "Account Settings", + href: "/account/edit", + }, + { + title: "Sign Out", + onClick: () => {}, + }, + ], + }, + { + title: "Nostrud exercitation", + subMenuLinks: [ + { + title: "My Dashboard", + href: "/account/dashboard", + }, + { + title: "My Applications", + href: "/account/dashboard", + }, + { + title: "Account Settings", + href: "/account/edit", + }, + { + title: "Sign Out", + onClick: () => {}, + }, + ], + }, + ]} + /> +) diff --git a/src/headers/SiteHeader.tsx b/src/headers/SiteHeader.tsx index e1258f4..edf235c 100644 --- a/src/headers/SiteHeader.tsx +++ b/src/headers/SiteHeader.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useContext } from "react" +import React, { useState, useContext, useLayoutEffect } from "react" import { CSSTransition } from "react-transition-group" import { LanguageNav, LangItem } from "../navigation/LanguageNav" import { Icon } from "../icons/Icon" @@ -51,33 +51,43 @@ export interface SiteHeaderProps { } const SiteHeader = (props: SiteHeaderProps) => { - const [activeMenus, setActiveMenus] = useState([]) + const [activeMenu, setActiveMenu] = useState() const [activeMobileMenus, setActiveMobileMenus] = useState([]) const [isDesktop, setIsDesktop] = useState(true) const [mobileDrawer, setMobileDrawer] = useState(false) const [mobileMenu, setMobileMenu] = useState(false) + const [navbarClass, setNavbarClass] = useState("site-header__navbar-inline") + + const updateNavbarClass = () => { + // If the links have flex-wrapped onto the next line, apply the background color + const logoOffset = document.getElementById("site-header-logo")?.offsetLeft + const linksOffset = document.getElementById("site-header-links")?.offsetLeft + if (linksOffset === undefined || logoOffset === undefined) return + return linksOffset === 0 || linksOffset === logoOffset + ? setNavbarClass("site-header__navbar-wrapped") + : setNavbarClass("site-header__navbar-inline") + } + const { LinkComponent } = useContext(NavigationContext) const DESKTOP_MIN_WIDTH = props.desktopMinWidth || 767 // @screen md // Enables toggling off navbar links when entering mobile - useEffect(() => { - if (window.innerWidth > DESKTOP_MIN_WIDTH) { - setIsDesktop(true) - } else { - setIsDesktop(false) - } - + useLayoutEffect(() => { const updateMedia = () => { if (window.innerWidth > DESKTOP_MIN_WIDTH) { setIsDesktop(true) } else { setIsDesktop(false) } + updateNavbarClass() } + + updateMedia() + window.addEventListener("resize", updateMedia) return () => window.removeEventListener("resize", updateMedia) - }, [DESKTOP_MIN_WIDTH]) + }, [DESKTOP_MIN_WIDTH, props.languages]) const getLogoWidthClass = () => { if (props.logoWidth === "slim") return "site-header__logo-width-slim" @@ -100,13 +110,12 @@ const SiteHeader = (props: SiteHeaderProps) => { parentMenu?: string ) => { const dropdownOptionKeyDown = (event: React.KeyboardEvent, index: number) => { - // Close menu when tabbing out backwards - if (event.shiftKey && event.key === "Tab" && isDesktop && index === 0 && parentMenu) { - changeMenuShow(parentMenu, activeMenus, setActiveMenus) - } - // Close menu when tabbing out forwards - if (event.key === "Tab" && isDesktop && index === options.length - 1 && parentMenu) { - changeMenuShow(parentMenu, activeMenus, setActiveMenus) + // Close menu when tabbing out backwards or forwards + if ( + (event.shiftKey && event.key === "Tab" && isDesktop && index === 0 && parentMenu) || + (event.key === "Tab" && isDesktop && index === options.length - 1 && parentMenu) + ) { + setActiveMenu(null) } } @@ -169,11 +178,28 @@ const SiteHeader = (props: SiteHeaderProps) => { // Render the desktop dropdown that opens on mouse hover const getDesktopDropdown = (menuTitle: string, subMenus: MenuLink[]) => { + // Combine the last word of a multi-word header into one span to prevent chevron icon from wrapping onto its own line + const getMenuTitle = () => { + const splitTitle = menuTitle.split(" ") + return ( + + {splitTitle.length > 1 && ( + + {[...splitTitle].splice(0, splitTitle.length - 1).join(" ")} + + )} + + {splitTitle.length > 1 ? splitTitle[splitTitle.length - 1] : menuTitle} + + + + ) + } + return ( - {menuTitle} - - {activeMenus.indexOf(menuTitle) >= 0 && ( + {getMenuTitle()} + {activeMenu === menuTitle && (
{getDropdownOptions(subMenus, "site-header__dropdown-item", menuTitle)} @@ -191,6 +217,14 @@ const SiteHeader = (props: SiteHeaderProps) => { dropdownOptionClassName: string, dropdownContainerClassName?: string ) => { + const changeMenuShow = ( + title: string, + menus: string[], + setMenus: React.Dispatch> + ) => { + const indexOfTitle = menus.indexOf(title) + setMenus(indexOfTitle >= 0 ? menus.filter((menu) => menu !== title) : [...menus, title]) + } return ( <> {menuLinks.map((menuLink, index) => { @@ -298,14 +332,6 @@ const SiteHeader = (props: SiteHeaderProps) => { ) } - const changeMenuShow = ( - title: string, - menus: string[], - setMenus: React.Dispatch> - ) => { - const indexOfTitle = menus.indexOf(title) - setMenus(indexOfTitle >= 0 ? menus.filter((menu) => menu !== title) : [...menus, title]) - } const getDesktopHeader = () => { return ( @@ -361,11 +387,15 @@ const SiteHeader = (props: SiteHeaderProps) => { key={`${menuLink.title}-${index}`} onKeyPress={(event) => { if (event.key === "Enter") { - changeMenuShow(menuLink.title, activeMenus, setActiveMenus) + setActiveMenu(menuLink.title) } }} - onMouseEnter={() => changeMenuShow(menuLink.title, activeMenus, setActiveMenus)} - onMouseLeave={() => changeMenuShow(menuLink.title, activeMenus, setActiveMenus)} + onMouseEnter={() => { + setActiveMenu(menuLink.title) + }} + onMouseLeave={() => { + setActiveMenu(null) + }} role={"button"} data-testid={`${menuLink.title}-${index}`} > @@ -441,7 +471,7 @@ const SiteHeader = (props: SiteHeaderProps) => { const getLogo = () => { return ( -
+
{
{getLogo()} -
+
{isDesktop ? getDesktopHeader() : getMobileHeader()}