feat(SW-184): language switcher mobile/desktop functionality
This commit is contained in:
@@ -7,6 +7,7 @@ import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons"
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
|
||||
import HeaderLink from "../../HeaderLink"
|
||||
import NavigationMenu from "../NavigationMenu"
|
||||
@@ -22,6 +23,12 @@ export default function MobileMenu({
|
||||
const intl = useIntl()
|
||||
const { isHamburgerMenuOpen, toggleHamburgerMenu } = useDropdownStore()
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isHamburgerMenuOpen) {
|
||||
toggleHamburgerMenu()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -33,11 +40,7 @@ export default function MobileMenu({
|
||||
>
|
||||
<span className={styles.bar}></span>
|
||||
</button>
|
||||
<Modal
|
||||
className={styles.modal}
|
||||
isOpen={isHamburgerMenuOpen}
|
||||
onOpenChange={toggleHamburgerMenu}
|
||||
>
|
||||
<Modal className={styles.modal} isOpen={isHamburgerMenuOpen}>
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({ id: "Menu" })}
|
||||
|
||||
@@ -80,11 +80,12 @@
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
right: auto;
|
||||
top: var(--main-menu-mobile-height);
|
||||
right: auto;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
transition: right 0.3s;
|
||||
}
|
||||
|
||||
.modal[data-entering] {
|
||||
|
||||
@@ -7,27 +7,27 @@ export default function CheckIcon({ className, color, ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
fill="none"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
height="25"
|
||||
id="mask0_1333_19690"
|
||||
maskUnits="userSpaceOnUse"
|
||||
id="mask0_2570_1776"
|
||||
style={{ maskType: "alpha" }}
|
||||
width="24"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<rect y="0.629639" width="24" height="24" fill="#D9D9D9" />
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_1333_19690)">
|
||||
<g mask="url(#mask0_2570_1776)">
|
||||
<path
|
||||
d="M9.99967 16.6738L6.35547 13.0296L7.39967 11.9854L9.99967 14.5854L16.5997 7.98535L17.6439 9.02955L9.99967 16.6738Z"
|
||||
d="M9.57552 15.1752L17.9505 6.8002C18.1422 6.60853 18.3651 6.5127 18.6193 6.5127C18.8734 6.5127 19.0964 6.60853 19.288 6.8002C19.4797 6.99186 19.5755 7.21478 19.5755 7.46895C19.5755 7.72311 19.4797 7.94603 19.288 8.1377L10.238 17.1877C10.0464 17.3794 9.82552 17.4752 9.57552 17.4752C9.32552 17.4752 9.10469 17.3794 8.91302 17.1877L4.71302 12.9877C4.52136 12.796 4.42761 12.5731 4.43177 12.3189C4.43594 12.0648 4.53386 11.8419 4.72552 11.6502C4.91719 11.4585 5.14011 11.3627 5.39427 11.3627C5.64844 11.3627 5.87136 11.4585 6.06302 11.6502L9.57552 15.1752Z"
|
||||
fill="#4D001B"
|
||||
/>
|
||||
</g>
|
||||
|
||||
40
components/Icons/ChevronLeft.tsx
Normal file
40
components/Icons/ChevronLeft.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function ChevronLeftIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_2291_1760"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_2291_1760)">
|
||||
<path
|
||||
d="M7.75 10.0001L13.5729 15.823C13.7535 16.0036 13.8455 16.2258 13.849 16.4897C13.8524 16.7536 13.7639 16.9758 13.5833 17.1563C13.4028 17.3369 13.1806 17.4272 12.9167 17.4272C12.6528 17.4272 12.4306 17.3369 12.25 17.1563L6.08333 10.9793C5.9375 10.8334 5.83507 10.6807 5.77604 10.5209C5.71701 10.3612 5.6875 10.1876 5.6875 10.0001C5.6875 9.8126 5.71701 9.63899 5.77604 9.47927C5.83507 9.31954 5.9375 9.16677 6.08333 9.02093L12.25 2.85426C12.4306 2.67371 12.651 2.5817 12.9115 2.57822C13.1719 2.57475 13.3924 2.66329 13.5729 2.84385C13.7535 3.0244 13.8438 3.24663 13.8438 3.51051C13.8438 3.7744 13.7535 3.99663 13.5729 4.17718L7.75 10.0001Z"
|
||||
fill="#CD0921"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CloseIcon,
|
||||
CloseLarge,
|
||||
@@ -75,6 +76,8 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
|
||||
return CheckCircleIcon
|
||||
case IconName.ChevronDown:
|
||||
return ChevronDownIcon
|
||||
case IconName.ChevronLeft:
|
||||
return ChevronLeftIcon
|
||||
case IconName.ChevronRight:
|
||||
return ChevronRightIcon
|
||||
case IconName.Close:
|
||||
|
||||
@@ -9,6 +9,7 @@ export { default as CellphoneIcon } from "./Cellphone"
|
||||
export { default as CheckIcon } from "./Check"
|
||||
export { default as CheckCircleIcon } from "./CheckCircle"
|
||||
export { default as ChevronDownIcon } from "./ChevronDown"
|
||||
export { default as ChevronLeftIcon } from "./ChevronLeft"
|
||||
export { default as ChevronRightIcon } from "./ChevronRight"
|
||||
export { default as CloseIcon } from "./Close"
|
||||
export { default as CloseLarge } from "./CloseLarge"
|
||||
|
||||
@@ -1,28 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Lang, languages } from "@/constants/languages"
|
||||
import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import { CheckIcon, ChevronDownIcon, GlobeIcon } from "@/components/Icons"
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
GlobeIcon,
|
||||
} from "@/components/Icons"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { useTrapFocus } from "@/hooks/useTrapFocus"
|
||||
|
||||
import Subtitle from "../TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./languageSwitcher.module.css"
|
||||
|
||||
import { LanguageSwitcherProps } from "@/types/components/current/languageSwitcher"
|
||||
|
||||
export default function LanguageSwitcher({ urls }: LanguageSwitcherProps) {
|
||||
export default function LanguageSwitcher({
|
||||
urls,
|
||||
location = "header",
|
||||
}: LanguageSwitcherProps) {
|
||||
const intl = useIntl()
|
||||
const languageSwitcherRef = useTrapFocus()
|
||||
const currentLanguage = useLang()
|
||||
const { toggleLanguageSwitcher, isLanguageSwitcherOpen } = useDropdownStore()
|
||||
|
||||
const urlKeys = Object.keys(urls) as Lang[]
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isLanguageSwitcherOpen) {
|
||||
toggleLanguageSwitcher()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.languageSwitcher}>
|
||||
<div className={styles.languageSwitcher} ref={languageSwitcherRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.button}
|
||||
aria-label={intl.formatMessage({
|
||||
id: isLanguageSwitcherOpen
|
||||
? "Close language menu"
|
||||
: "Open language menu",
|
||||
})}
|
||||
onClick={toggleLanguageSwitcher}
|
||||
>
|
||||
<GlobeIcon width={20} height={20} color="burgundy" />
|
||||
@@ -38,26 +64,42 @@ export default function LanguageSwitcher({ urls }: LanguageSwitcherProps) {
|
||||
<div
|
||||
className={`${styles.dropdown} ${isLanguageSwitcherOpen ? styles.isExpanded : ""}`}
|
||||
>
|
||||
<ul className={styles.list}>
|
||||
{urlKeys.map((key) => {
|
||||
const url = urls[key]?.url
|
||||
const isActive = currentLanguage === key
|
||||
if (url) {
|
||||
return (
|
||||
<li key={key}>
|
||||
<Link
|
||||
className={`${styles.link} ${isActive ? styles.active : ""}`}
|
||||
color="burgundy"
|
||||
href={url}
|
||||
>
|
||||
{languages[key]}
|
||||
{isActive ? <CheckIcon color="burgundy" /> : null}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
<div className={styles.backWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.backButton}
|
||||
onClick={toggleLanguageSwitcher}
|
||||
>
|
||||
<ChevronLeftIcon color="red" />
|
||||
<Subtitle type="one">Main Menu</Subtitle>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.languageWrapper}>
|
||||
<Subtitle className={styles.subtitle} type="two">
|
||||
{intl.formatMessage({ id: "Select your language" })}
|
||||
</Subtitle>
|
||||
<ul className={styles.list}>
|
||||
{urlKeys.map((key) => {
|
||||
const url = urls[key]?.url
|
||||
const isActive = currentLanguage === key
|
||||
if (url) {
|
||||
return (
|
||||
<li key={key}>
|
||||
<Link
|
||||
className={`${styles.link} ${isActive ? styles.active : ""}`}
|
||||
color="burgundy"
|
||||
href={url}
|
||||
>
|
||||
{languages[key]}
|
||||
{isActive ? <CheckIcon color="burgundy" /> : null}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
window.removeEventListener("keydown", callback)
|
||||
}
|
||||
}, [callback])
|
||||
}
|
||||
|
||||
12
hooks/useHandleKeyUp.ts
Normal file
12
hooks/useHandleKeyUp.ts
Normal file
@@ -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])
|
||||
}
|
||||
82
hooks/useTrapFocus.ts
Normal file
82
hooks/useTrapFocus.ts
Normal file
@@ -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<HTMLDivElement>(null)
|
||||
const previouseFocusedElement = useRef<HTMLElement>(
|
||||
document.activeElement as HTMLElement
|
||||
)
|
||||
const [tabbableElements, setTabbableElements] = useState<HTMLElement[]>([])
|
||||
// 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)
|
||||
}
|
||||
@@ -19,7 +19,15 @@ const useDropdownStore = create<DropdownState>((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<DropdownState>((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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export enum IconName {
|
||||
CrossCircle = "CrossCircle",
|
||||
CheckCircle = "CheckCircle",
|
||||
ChevronDown = "ChevronDown",
|
||||
ChevronLeft = "ChevronLeft",
|
||||
ChevronRight = "ChevronRight",
|
||||
Close = "Close",
|
||||
CloseLarge = "CloseLarge",
|
||||
|
||||
62
utils/tabbable.ts
Normal file
62
utils/tabbable.ts
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user