From d50cf829e62193bd29d2e8004aaab2984d12a7f7 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Wed, 12 Mar 2025 15:16:06 +0000 Subject: [PATCH] feat(SW-1842): Making the navigation links in the header render in the initial HTML for SEO purposes * feat(SW-1842): Making the navigation links in the header render in the initial HTML for SEO purposes Approved-by: Fredrik Thorsson --- .../MainMenu/MyPagesMenuContent/index.tsx | 63 +++---- .../NavigationMenu/MegaMenu/index.tsx | 172 +++++++++--------- .../MegaMenu/megaMenu.module.css | 4 + .../NavigationMenuItem/index.tsx | 17 +- .../LanguageSwitcherContent/index.tsx | 59 +++--- apps/scandic-web/hooks/useTrapFocus.ts | 83 --------- apps/scandic-web/package.json | 1 + .../types/components/header/megaMenu.ts | 1 + yarn.lock | 32 ++++ 9 files changed, 197 insertions(+), 235 deletions(-) delete mode 100644 apps/scandic-web/hooks/useTrapFocus.ts diff --git a/apps/scandic-web/components/Header/MainMenu/MyPagesMenuContent/index.tsx b/apps/scandic-web/components/Header/MainMenu/MyPagesMenuContent/index.tsx index e7310c096..00e9bf402 100644 --- a/apps/scandic-web/components/Header/MainMenu/MyPagesMenuContent/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/MyPagesMenuContent/index.tsx @@ -1,5 +1,6 @@ "use client" +import { FocusTrap } from "focus-trap-react" import { useIntl } from "react-intl" import { logout } from "@/constants/routes/handleAuth" @@ -11,7 +12,6 @@ import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" -import { useTrapFocus } from "@/hooks/useTrapFocus" import styles from "./myPagesMenuContent.module.css" @@ -26,7 +26,6 @@ export default function MyPagesMenuContent({ membershipLevel, }: Props) { const intl = useIntl() - const myPagesMenuContentRef = useTrapFocus() const { data: myPagesNavigation } = useMyPagesNavigation() const primaryLinks = myPagesNavigation?.primaryLinks ?? [] @@ -43,38 +42,40 @@ export default function MyPagesMenuContent({ } return ( - + ) } diff --git a/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx b/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx index d495924c4..553d6021d 100644 --- a/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx @@ -1,5 +1,7 @@ "use client" +import { FocusTrap } from "focus-trap-react" + import useDropdownStore from "@/stores/main-menu" import { ArrowRightIcon, ChevronLeftIcon } from "@/components/Icons" @@ -7,7 +9,6 @@ import Card from "@/components/TempDesignSystem/Card" import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { useTrapFocus } from "@/hooks/useTrapFocus" import styles from "./megaMenu.module.css" @@ -20,10 +21,10 @@ export default function MegaMenu({ seeAllLink, submenu, card, + isOpen, }: MegaMenuProps) { const { toggleMegaMenu, toggleDropdown, isHamburgerMenuOpen } = useDropdownStore() - const megaMenuRef = useTrapFocus() function handleNavigate() { toggleMegaMenu(false) @@ -33,89 +34,94 @@ export default function MegaMenu({ } return ( - +
+
+ {seeAllLink?.link ? ( + + {seeAllLink.title} + + + ) : null} +
+ + {card ? ( +
+ +
+ ) : null} +
+ + ) } diff --git a/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/megaMenu.module.css b/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/megaMenu.module.css index da3810dc9..6b5052e9e 100644 --- a/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/megaMenu.module.css +++ b/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/megaMenu.module.css @@ -1,3 +1,7 @@ +.megaMenu:not(.active) { + display: none; +} + .megaMenuContent { display: grid; } diff --git a/apps/scandic-web/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx b/apps/scandic-web/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx index a8e87c740..22725115b 100644 --- a/apps/scandic-web/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx @@ -66,15 +66,14 @@ export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { ref={megaMenuRef} className={`${styles.dropdown} ${isMegaMenuOpen ? styles.isExpanded : ""}`} > - {isMegaMenuOpen ? ( - - ) : null} + ) : ( diff --git a/apps/scandic-web/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx b/apps/scandic-web/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx index 044713293..d2e9b882c 100644 --- a/apps/scandic-web/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx +++ b/apps/scandic-web/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx @@ -1,5 +1,6 @@ "use client" +import { FocusTrap } from "focus-trap-react" import { usePathname } from "next/navigation" import { useIntl } from "react-intl" @@ -9,7 +10,6 @@ import { CheckIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" -import { useTrapFocus } from "@/hooks/useTrapFocus" import { replaceUrlPart } from "./utils" @@ -24,39 +24,40 @@ export default function LanguageSwitcherContent({ const intl = useIntl() const currentLanguage = useLang() - const languageSwitcherRef = useTrapFocus() const urlKeys = Object.keys(urls) as Lang[] const pathname = usePathname() return ( -
-
- - {intl.formatMessage({ id: "Select your language" })} - -
    - {urlKeys.map((key) => { - const url = urls[key]?.url - const isActive = currentLanguage === key - if (url) { - return ( -
  • - - {languages[key]} - {isActive ? : null} - -
  • - ) - } - })} -
+ +
+
+ + {intl.formatMessage({ id: "Select your language" })} + +
    + {urlKeys.map((key) => { + const url = urls[key]?.url + const isActive = currentLanguage === key + if (url) { + return ( +
  • + + {languages[key]} + {isActive ? : null} + +
  • + ) + } + })} +
+
-
+ ) } diff --git a/apps/scandic-web/hooks/useTrapFocus.ts b/apps/scandic-web/hooks/useTrapFocus.ts deleted file mode 100644 index b7d1311c8..000000000 --- a/apps/scandic-web/hooks/useTrapFocus.ts +++ /dev/null @@ -1,83 +0,0 @@ -"use client" - -import { useCallback, useEffect, useRef, useState } from "react" - -import { useHandleKeyPress } from "@/hooks/useHandleKeyPress" -import findTabbableDescendants from "@/utils/tabbable" - -const TAB_KEY = "Tab" -const optionsDefault = { focusOnRender: true, returnFocus: true } -type OptionsType = { - focusOnRender?: boolean - returnFocus?: boolean -} -export function useTrapFocus(opts?: OptionsType) { - const options = opts ? { ...optionsDefault, ...opts } : optionsDefault - const ref = useRef(null) - const previouseFocusedElement = useRef( - document.activeElement as HTMLElement - ) - const [tabbableElements, setTabbableElements] = useState([]) - // Handle initial focus of the referenced element, and return focus to previously focused element on cleanup - // and find all the tabbable elements in the referenced element - - useEffect(() => { - const { current } = ref - if (current) { - const focusableChildNodes = findTabbableDescendants(current) - if (options.focusOnRender) { - current.focus() - } - - setTabbableElements(focusableChildNodes) - } - return () => { - const { current } = previouseFocusedElement - if (current instanceof HTMLElement && options.returnFocus) { - current.focus() - } - } - }, [options.focusOnRender, options.returnFocus, ref, setTabbableElements]) - - const handleUserKeyPress = useCallback( - (event: KeyboardEvent) => { - const { code, shiftKey } = event - const first = tabbableElements[0] - const last = tabbableElements[tabbableElements.length - 1] - - const currentActiveElement = document.activeElement - // Scope current tabs to current root element - if (isWithinCurrentElementScope([...tabbableElements, ref.current])) { - if (code === TAB_KEY) { - if ( - currentActiveElement === first || - currentActiveElement === ref.current - ) { - // move focus to last element if shift+tab while currently focusing the first tabbable element - if (shiftKey) { - event.preventDefault() - last.focus() - } - } - if (currentActiveElement === last) { - // move focus back to first if tabbing while currently focusing the last tabbable element - if (!shiftKey) { - event.preventDefault() - first.focus() - } - } - } - } - }, - [ref, tabbableElements] - ) - useHandleKeyPress(handleUserKeyPress) - - return ref -} -function isWithinCurrentElementScope( - elementList: (HTMLInputElement | Element | null)[] -) { - const currentActiveElement = document.activeElement - return elementList.includes(currentActiveElement) -} diff --git a/apps/scandic-web/package.json b/apps/scandic-web/package.json index 3a60866b7..b23cc71cf 100644 --- a/apps/scandic-web/package.json +++ b/apps/scandic-web/package.json @@ -68,6 +68,7 @@ "embla-carousel-react": "^8.5.2", "fast-deep-equal": "^3.1.3", "fetch-retry": "^6.0.0", + "focus-trap-react": "^11.0.3", "framer-motion": "^11.3.28", "graphql": "^16.8.1", "graphql-request": "^6.1.0", diff --git a/apps/scandic-web/types/components/header/megaMenu.ts b/apps/scandic-web/types/components/header/megaMenu.ts index 8eab851f3..924dbb654 100644 --- a/apps/scandic-web/types/components/header/megaMenu.ts +++ b/apps/scandic-web/types/components/header/megaMenu.ts @@ -6,4 +6,5 @@ export interface MegaMenuProps { submenu: MenuItem["submenu"] card: MenuItem["card"] isMobile: boolean + isOpen: boolean } diff --git a/yarn.lock b/yarn.lock index 74f7b5c8e..85d45f9de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6117,6 +6117,7 @@ __metadata: eslint-plugin-simple-import-sort: "npm:^12.1.0" fast-deep-equal: "npm:^3.1.3" fetch-retry: "npm:^6.0.0" + focus-trap-react: "npm:^11.0.3" framer-motion: "npm:^11.3.28" graphql: "npm:^16.8.1" graphql-request: "npm:^6.1.0" @@ -12190,6 +12191,30 @@ __metadata: languageName: node linkType: hard +"focus-trap-react@npm:^11.0.3": + version: 11.0.3 + resolution: "focus-trap-react@npm:11.0.3" + dependencies: + focus-trap: "npm:^7.6.4" + tabbable: "npm:^6.2.0" + peerDependencies: + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + checksum: 10c0/5a4829720901e32ef8cb0783a91e06262e079ab4a0537ffa21d866b1b70d6d383a4ee14baa19e84d952a304b4b08814a1c1fbdb2307d79a2995977735376ae45 + languageName: node + linkType: hard + +"focus-trap@npm:^7.6.4": + version: 7.6.4 + resolution: "focus-trap@npm:7.6.4" + dependencies: + tabbable: "npm:^6.2.0" + checksum: 10c0/ed810d47fd904a5e0269e822d98e634c6cbdd7222046c712ef299bdd26a422db754e3cec04e6517065b12be4b47f65c21f6244e0c07a308b1060985463d518cb + languageName: node + linkType: hard + "follow-redirects@npm:^1.15.6": version: 1.15.9 resolution: "follow-redirects@npm:1.15.9" @@ -19533,6 +19558,13 @@ __metadata: languageName: node linkType: hard +"tabbable@npm:^6.2.0": + version: 6.2.0 + resolution: "tabbable@npm:6.2.0" + checksum: 10c0/ced8b38f05f2de62cd46836d77c2646c42b8c9713f5bd265daf0e78ff5ac73d3ba48a7ca45f348bafeef29b23da7187c72250742d37627883ef89cbd7fa76898 + languageName: node + linkType: hard + "table-layout@npm:^1.0.2, table-layout@npm:~1.0.0": version: 1.0.2 resolution: "table-layout@npm:1.0.2"