Files
Portfolio/src/components/layout/LanguageSwitcher.tsx
T
damjan_savicandClaude Sonnet 4.5 5b4c24e737 auto-claude: subtask-1-1 - Add focusedIndex state and arrow key navigation logic
- Added focusedIndex state to track keyboard focus
- Enhanced handleKeyDown to support ArrowDown, ArrowUp, Enter, and Escape keys
- Added visual focus indicator with orange ring (ring-2 ring-orange-500)
- Implemented aria-activedescendant for screen reader support
- Added unique IDs to language options for accessibility
- Reset focusedIndex when dropdown opens/closes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-25 06:32:13 +01:00

135 lines
4.3 KiB
TypeScript

'use client';
import { useRef, useState, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useLocale } from 'next-intl';
import { Globe } from 'lucide-react';
const LanguageSwitcher = () => {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [focusedIndex, setFocusedIndex] = useState<number>(-1);
const dropdownRef = useRef<HTMLDivElement>(null);
const languages = [
{ code: 'en', name: 'English' },
{ code: 'de', name: 'Deutsch' },
{ code: 'sr', name: 'Srpski' },
];
const currentLanguage = languages.find(lang => lang.code === locale)?.name || 'Language';
const handleLanguageChange = (newLocale: string) => {
// Replace the locale in the pathname
const pathWithoutLocale = pathname.replace(/^\/(de|en|sr)/, '');
const newPath = `/${newLocale}${pathWithoutLocale || ''}`;
router.push(newPath);
setIsOpen(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (!isOpen) return;
switch (e.key) {
case 'Escape':
setIsOpen(false);
setFocusedIndex(-1);
break;
case 'ArrowDown':
e.preventDefault();
setFocusedIndex(prev => (prev + 1) % languages.length);
break;
case 'ArrowUp':
e.preventDefault();
setFocusedIndex(prev => (prev - 1 + languages.length) % languages.length);
break;
case 'Enter':
e.preventDefault();
if (focusedIndex >= 0) {
handleLanguageChange(languages[focusedIndex].code);
setFocusedIndex(-1);
}
break;
}
};
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
document.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('resize', checkMobile);
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative inline-block" ref={dropdownRef}>
<button
className="flex items-center space-x-2 text-zinc-400 hover:text-white
px-4 py-2 rounded-full transition-all duration-200
hover:bg-zinc-800/50 border border-transparent hover:border-zinc-800
hover:scale-105 active:scale-95"
onClick={() => {
setIsOpen(!isOpen);
setFocusedIndex(-1);
}}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-label="Select language"
>
<Globe className="h-5 w-5" aria-hidden="true" />
<span className="text-sm">{currentLanguage}</span>
</button>
<div
className={`absolute ${isMobile ? 'bottom-full mb-2' : 'top-full mt-2'} right-0 w-48 rounded-lg overflow-hidden
border border-zinc-800 bg-zinc-900/95 backdrop-blur-sm shadow-lg
transition-all duration-200 origin-top
${isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`}
role="listbox"
aria-label="Languages"
aria-activedescendant={focusedIndex >= 0 ? `lang-option-${languages[focusedIndex].code}` : undefined}
onKeyDown={handleKeyDown}
>
<div className="py-1">
{languages.map((lang, index) => (
<button
key={lang.code}
id={`lang-option-${lang.code}`}
className={`w-full text-left px-4 py-2 text-sm transition-colors duration-200
hover:bg-zinc-800/50
${locale === lang.code
? 'bg-zinc-800 text-white'
: 'text-zinc-400 hover:text-white'
}
${focusedIndex === index ? 'ring-2 ring-orange-500 ring-inset' : ''}`}
onClick={() => handleLanguageChange(lang.code)}
role="option"
aria-selected={locale === lang.code}
>
{lang.name}
</button>
))}
</div>
</div>
</div>
);
};
export default LanguageSwitcher;