+
)
diff --git a/components/LanguageSwitcher/languageSwitcher.module.css b/components/LanguageSwitcher/languageSwitcher.module.css
index 1eeeab77b..9bdf4f188 100644
--- a/components/LanguageSwitcher/languageSwitcher.module.css
+++ b/components/LanguageSwitcher/languageSwitcher.module.css
@@ -1,5 +1,11 @@
-.languageSwitcher {
- position: relative;
+@keyframes slide-in {
+ from {
+ right: -100vw;
+ }
+
+ to {
+ right: 0;
+ }
}
.button {
@@ -27,33 +33,50 @@
}
.dropdown {
- position: absolute;
- top: 2.25rem;
- right: 0;
+ position: fixed;
+ top: var(--main-menu-mobile-height);
+ right: -100vw;
+ bottom: 0;
+ width: 100%;
background-color: var(--Base-Surface-Primary-light-Normal);
- padding: var(--Spacing-x2) var(--Spacing-x3);
- border-radius: var(--Corner-radius-Large);
- box-shadow: 0px 0px 14px 6px #0000001a;
- display: none;
- min-width: 12.5rem;
- z-index: 1;
-}
-
-/* Triangle above dropdown */
-.dropdown::before {
- content: "";
- position: absolute;
- top: -1.25rem;
- right: 2.4rem;
- transform: rotate(180deg);
- border-width: 0.75rem;
- border-style: solid;
- border-color: var(--Base-Surface-Primary-light-Normal) transparent transparent
- transparent;
+ transition: right 0.3s;
}
.dropdown.isExpanded {
display: block;
+ right: 0;
+}
+
+.backWrapper {
+ background-color: var(--Base-Surface-Secondary-light-Normal);
+ padding: var(--Spacing-x2);
+}
+
+.backButton {
+ background-color: transparent;
+ border: none;
+ color: var(--Base-Text-High-contrast);
+ font-family: var(--typography-Subtitle-1-fontFamily);
+ font-weight: var(--typography-Subtitle-1-fontWeight);
+ font-size: var(--typography-Subtitle-1-Mobile-fontSize);
+ padding: 0;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: var(--Spacing-x1);
+}
+
+.languageWrapper {
+ display: grid;
+ gap: var(--Spacing-x3);
+ padding: var(--Spacing-x3) var(--Spacing-x2);
+}
+
+.subtitle {
+ font-family: var(--typography-Subtitle-2-fontFamily);
+ font-size: var(--typography-Subtitle-2-Mobile-fontSize);
+ font-weight: var(--typography-Subtitle-2-fontWeight);
+ color: var(--Base-Text-High-contrast, #4d001b);
}
.list {
@@ -73,22 +96,64 @@
justify-content: space-between;
align-items: center;
text-decoration: none;
-}
-
-.link:hover {
- background-color: var(--Base-Surface-Primary-light-Hover-alt);
border-radius: var(--Corner-radius-Medium);
}
.link.active,
.link:hover {
+ background-color: var(--Base-Surface-Primary-light-Hover-alt);
font-weight: 600;
}
@media screen and (min-width: 768px) {
+ .languageSwitcher {
+ position: relative;
+ }
+
+ .backWrapper {
+ display: none;
+ }
+
+ .languageWrapper {
+ padding: var(--Spacing-x2) var(--Spacing-x3);
+ }
+
+ .subtitle {
+ display: none;
+ }
+
+ .dropdown {
+ position: absolute;
+ top: 2.25rem;
+ background-color: var(--Base-Surface-Primary-light-Normal);
+ border-radius: var(--Corner-radius-Large);
+ box-shadow: 0px 0px 14px 6px #0000001a;
+ display: none;
+ min-width: 12.5rem;
+ z-index: 1;
+ bottom: auto;
+ }
+
+ /* Triangle above dropdown */
+ .dropdown::before {
+ content: "";
+ position: absolute;
+ top: -1.25rem;
+ right: 2.4rem;
+ transform: rotate(180deg);
+ border-width: 0.75rem;
+ border-style: solid;
+ border-color: var(--Base-Surface-Primary-light-Normal) transparent
+ transparent transparent;
+ }
+
.button {
grid-template-columns: repeat(3, max-content);
font-size: var(--typography-Body-Bold-fontSize);
font-family: var(--typography-Body-Bold-fontFamily);
}
+
+ .link.active:not(:hover) {
+ background-color: transparent;
+ }
}
diff --git a/hooks/useHandleKeyPress.ts b/hooks/useHandleKeyPress.ts
index d9fdc0ea2..b240650d1 100644
--- a/hooks/useHandleKeyPress.ts
+++ b/hooks/useHandleKeyPress.ts
@@ -1,11 +1,11 @@
"use client"
-import { useEffect } from 'react';
+import { useEffect } from "react"
export function useHandleKeyPress(callback: (event: KeyboardEvent) => void) {
useEffect(() => {
- window.addEventListener('keydown', callback);
+ window.addEventListener("keydown", callback)
return () => {
- window.removeEventListener('keydown', callback);
- };
- }, [callback]);
-}
\ No newline at end of file
+ window.removeEventListener("keydown", callback)
+ }
+ }, [callback])
+}
diff --git a/hooks/useHandleKeyUp.ts b/hooks/useHandleKeyUp.ts
new file mode 100644
index 000000000..b44f82d5a
--- /dev/null
+++ b/hooks/useHandleKeyUp.ts
@@ -0,0 +1,12 @@
+"use client"
+
+import { useEffect } from "react"
+
+export function useHandleKeyUp(callback: (event: KeyboardEvent) => void) {
+ useEffect(() => {
+ window.addEventListener("keyup", callback)
+ return () => {
+ window.removeEventListener("keyup", callback)
+ }
+ }, [callback])
+}
diff --git a/hooks/useTrapFocus.ts b/hooks/useTrapFocus.ts
new file mode 100644
index 000000000..7d2e214c7
--- /dev/null
+++ b/hooks/useTrapFocus.ts
@@ -0,0 +1,82 @@
+"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/stores/main-menu.ts b/stores/main-menu.ts
index 008cce9b7..1cfcd3039 100644
--- a/stores/main-menu.ts
+++ b/stores/main-menu.ts
@@ -19,7 +19,15 @@ const useDropdownStore = create((set) => ({
isMyPagesMenuOpen: false,
isLanguageSwitcherOpen: false,
toggleHamburgerMenu: () =>
- set((state) => ({ isHamburgerMenuOpen: !state.isHamburgerMenuOpen })),
+ set(({ isHamburgerMenuOpen, isMyPagesMenuOpen }) => {
+ // Close the other dropdown if it's open
+ if (!isHamburgerMenuOpen && isMyPagesMenuOpen) {
+ set({ isMyPagesMenuOpen: false })
+ }
+ return { isHamburgerMenuOpen: !isHamburgerMenuOpen }
+ }),
+ // toggleHamburgerMenu: () =>
+ // set((state) => ({ isHamburgerMenuOpen: !state.isHamburgerMenuOpen })),
toggleMyPagesMobileMenu: () =>
set((state) => {
// Close the other dropdown if it's open
@@ -29,13 +37,18 @@ const useDropdownStore = create((set) => ({
return { isMyPagesMobileMenuOpen: !state.isMyPagesMobileMenuOpen }
}),
toggleMyPagesMenu: () =>
- set(({ isLanguageSwitcherOpen, isMyPagesMenuOpen }) => {
- // Close the other dropdown if it's open
- if (!isMyPagesMenuOpen && isLanguageSwitcherOpen) {
- set({ isLanguageSwitcherOpen: false })
+ set(
+ ({ isHamburgerMenuOpen, isLanguageSwitcherOpen, isMyPagesMenuOpen }) => {
+ // Close the other dropdown if it's open
+ if (!isMyPagesMenuOpen && isLanguageSwitcherOpen) {
+ set({ isLanguageSwitcherOpen: false })
+ }
+ if (!isMyPagesMenuOpen && isHamburgerMenuOpen) {
+ set({ isHamburgerMenuOpen: false })
+ }
+ return { isMyPagesMenuOpen: !isMyPagesMenuOpen }
}
- return { isMyPagesMenuOpen: !isMyPagesMenuOpen }
- }),
+ ),
toggleLanguageSwitcher: () =>
set(({ isLanguageSwitcherOpen, isMyPagesMenuOpen }) => {
// Close the other dropdown if it's open
diff --git a/types/components/current/languageSwitcher.ts b/types/components/current/languageSwitcher.ts
index 0f409d36e..dd39494a9 100644
--- a/types/components/current/languageSwitcher.ts
+++ b/types/components/current/languageSwitcher.ts
@@ -1,5 +1,3 @@
-import { Lang } from "@/constants/languages"
-
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
export type LanguageSwitcherLink = {
@@ -9,4 +7,5 @@ export type LanguageSwitcherLink = {
export type LanguageSwitcherProps = {
urls: LanguageSwitcherData
+ location?: "header" | "footer"
}
diff --git a/types/components/icon.ts b/types/components/icon.ts
index 0e705b7e6..b0f6359d8 100644
--- a/types/components/icon.ts
+++ b/types/components/icon.ts
@@ -19,6 +19,7 @@ export enum IconName {
CrossCircle = "CrossCircle",
CheckCircle = "CheckCircle",
ChevronDown = "ChevronDown",
+ ChevronLeft = "ChevronLeft",
ChevronRight = "ChevronRight",
Close = "Close",
CloseLarge = "CloseLarge",
diff --git a/utils/tabbable.ts b/utils/tabbable.ts
new file mode 100644
index 000000000..b38352c99
--- /dev/null
+++ b/utils/tabbable.ts
@@ -0,0 +1,62 @@
+/*!
+ * Adapted from jQuery UI core
+ *
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/category/ui-core/
+ */
+
+const tabbableNode = /input|select|textarea|button|object/
+
+function hidesContents(element: HTMLElement) {
+ const zeroSize = element.offsetWidth <= 0 && element.offsetHeight <= 0
+
+ // If the node is empty, this is good enough
+ if (zeroSize && !element.innerHTML) return true
+
+ // Otherwise we need to check some styles
+ const style = window.getComputedStyle(element)
+ return zeroSize
+ ? style.getPropertyValue("overflow") !== "visible"
+ : style.getPropertyValue("display") === "none"
+}
+
+function visible(element: any) {
+ let parentElement = element
+ while (parentElement) {
+ if (parentElement === document.body) break
+ if (hidesContents(parentElement)) return false
+ parentElement = parentElement.parentNode
+ }
+ return true
+}
+
+export function focusable(element: HTMLElement, isTabIndexNotNaN: boolean) {
+ const nodeName = element.nodeName.toLowerCase()
+ const res =
+ //@ts-ignore
+ (tabbableNode.test(nodeName) && !element.disabled) ||
+ //@ts-ignore
+ (nodeName === "a" ? element.href || isTabIndexNotNaN : isTabIndexNotNaN)
+ return res && visible(element)
+}
+
+export function tabbable(element: HTMLElement) {
+ let tabIndex = element.getAttribute("tabindex")
+ //@ts-ignore
+ if (tabIndex === null) tabIndex = undefined
+ //@ts-ignore
+ const isTabIndexNaN = isNaN(tabIndex)
+ //@ts-ignore
+ return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN)
+}
+
+export default function findTabbableDescendants(
+ element: HTMLElement
+): HTMLElement[] {
+ return [].slice.call(element.querySelectorAll("*"), 0).filter(tabbable)
+}