|
1 | 1 | "use client"; |
2 | 2 |
|
| 3 | +import { useEffect, useRef, useState } from "react"; |
3 | 4 | import { useTranslation } from "./language-provider"; |
4 | 5 | import { cn } from "../lib/utils"; |
5 | 6 |
|
6 | 7 | export function LanguageSwitcher() { |
7 | 8 | const { locale, setLocale, locales, dir } = useTranslation(); |
| 9 | + const [open, setOpen] = useState(false); |
| 10 | + const wrapperRef = useRef<HTMLDivElement>(null); |
| 11 | + |
| 12 | + const currentLocale = locales.find((item) => item.value === locale); |
| 13 | + |
| 14 | + useEffect(() => { |
| 15 | + function onPointerDown(event: MouseEvent) { |
| 16 | + if (!wrapperRef.current?.contains(event.target as Node)) { |
| 17 | + setOpen(false); |
| 18 | + } |
| 19 | + } |
| 20 | + |
| 21 | + function onEscape(event: KeyboardEvent) { |
| 22 | + if (event.key === "Escape") { |
| 23 | + setOpen(false); |
| 24 | + } |
| 25 | + } |
| 26 | + |
| 27 | + document.addEventListener("mousedown", onPointerDown); |
| 28 | + document.addEventListener("keydown", onEscape); |
| 29 | + |
| 30 | + return () => { |
| 31 | + document.removeEventListener("mousedown", onPointerDown); |
| 32 | + document.removeEventListener("keydown", onEscape); |
| 33 | + }; |
| 34 | + }, []); |
| 35 | + |
| 36 | + function onSelect(nextLocale: (typeof locales)[number]["value"]) { |
| 37 | + setLocale(nextLocale); |
| 38 | + setOpen(false); |
| 39 | + } |
| 40 | + |
8 | 41 | return ( |
9 | | - <div className={cn("flex items-center gap-2 text-sm", dir === "rtl" && "flex-row-reverse")}> |
10 | | - <select |
11 | | - className="h-9 rounded-lg border border-input bg-background px-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/60" |
12 | | - value={locale} |
13 | | - onChange={(e) => setLocale(e.target.value as any)} |
| 42 | + <div |
| 43 | + ref={wrapperRef} |
| 44 | + dir={dir} |
| 45 | + className={cn("relative flex items-center gap-2 text-sm", dir === "rtl" && "flex-row-reverse")} |
| 46 | + > |
| 47 | + <button |
| 48 | + type="button" |
| 49 | + aria-haspopup="listbox" |
| 50 | + aria-expanded={open} |
| 51 | + aria-label="Select language" |
| 52 | + className={cn( |
| 53 | + "flex h-9 min-w-[140px] items-center justify-between rounded-lg border border-input bg-background px-3 text-sm text-foreground transition-colors", |
| 54 | + "focus:outline-none focus:ring-2 focus:ring-primary/60", |
| 55 | + open && "ring-2 ring-primary/60" |
| 56 | + )} |
| 57 | + onClick={() => setOpen((prev) => !prev)} |
14 | 58 | > |
15 | | - {locales.map((l) => ( |
16 | | - <option key={l.value} value={l.value}> |
17 | | - {l.label} |
18 | | - </option> |
19 | | - ))} |
20 | | - </select> |
| 59 | + <span>{currentLocale?.label ?? locale}</span> |
| 60 | + <svg |
| 61 | + viewBox="0 0 24 24" |
| 62 | + fill="none" |
| 63 | + aria-hidden="true" |
| 64 | + className={cn("h-4 w-4 transition-transform", open && "rotate-180")} |
| 65 | + > |
| 66 | + <path |
| 67 | + d="M6 9L12 15L18 9" |
| 68 | + stroke="currentColor" |
| 69 | + strokeWidth="1.8" |
| 70 | + strokeLinecap="round" |
| 71 | + strokeLinejoin="round" |
| 72 | + /> |
| 73 | + </svg> |
| 74 | + </button> |
| 75 | + |
| 76 | + {open && ( |
| 77 | + <ul |
| 78 | + role="listbox" |
| 79 | + aria-label="Language options" |
| 80 | + className={cn( |
| 81 | + "absolute top-full z-30 mt-1 max-h-60 min-w-[140px] overflow-auto rounded-lg border border-input bg-background p-1 text-sm text-popover-foreground shadow-md flex flex-col gap-1", |
| 82 | + dir === "rtl" ? "left-0" : "right-0" |
| 83 | + )} |
| 84 | + > |
| 85 | + {locales.map((item) => { |
| 86 | + const isSelected = item.value === locale; |
| 87 | + return ( |
| 88 | + <li key={item.value}> |
| 89 | + <button |
| 90 | + type="button" |
| 91 | + role="option" |
| 92 | + aria-selected={isSelected} |
| 93 | + className={cn( |
| 94 | + "flex w-full items-center rounded-md px-2 py-1.5 text-left transition-colors", |
| 95 | + "hover:bg-accent hover:text-accent-foreground", |
| 96 | + "focus:outline-none focus:ring-2 focus:ring-primary/40", |
| 97 | + isSelected && "bg-accent text-accent-foreground" |
| 98 | + )} |
| 99 | + onClick={() => onSelect(item.value)} |
| 100 | + > |
| 101 | + {item.label} |
| 102 | + </button> |
| 103 | + </li> |
| 104 | + ); |
| 105 | + })} |
| 106 | + </ul> |
| 107 | + )} |
21 | 108 | </div> |
22 | 109 | ); |
23 | 110 | } |
0 commit comments