1010 * (e.g. "cta/" for CTA page so entries link to /{locale}/cta/).
1111 * For English ("en"), the root is "/" so "/cta/" is the result.
1212 */
13- import { languages } from ' ../i18n/config' ;
13+ import { languages , isRtl } from ' ../i18n/config' ;
1414
1515interface Props {
1616 locale: string ;
@@ -20,6 +20,7 @@ interface Props {
2020const { locale, suffix = ' ' } = Astro .props ;
2121const currentLabel = languages [locale as keyof typeof languages ]?.label ?? ' English' ;
2222const entries = Object .entries (languages ) as [string , { label: string ; path: string }][];
23+ const menuId = ` language-picker-${locale }-${suffix || ' home' } ` .replace (/ [^ a-zA-Z0-9 _-] / g , ' -' );
2324
2425// Build the URL for a given locale, honoring the suffix
2526function localeHref(code : string , basePath : string ): string {
@@ -30,19 +31,31 @@ function localeHref(code: string, basePath: string): string {
3031---
3132
3233<div class =" lang-picker" data-current ={ locale } >
33- <button class =" lang-picker-btn" type =" button" aria-expanded =" false" >
34- <span class =" lang-picker-globe" >🌐 </span >
34+ <button class =" lang-picker-btn" type =" button" aria-expanded =" false" aria-controls ={ menuId } aria-haspopup =" true" >
35+ <svg class =" lang-picker-globe" viewBox =" 0 0 24 24" width =" 18" height =" 18" fill =" none" stroke =" currentColor" stroke-width =" 1.8" aria-hidden =" true" >
36+ <circle cx =" 12" cy =" 12" r =" 9" ></circle >
37+ <path d =" M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" ></path >
38+ </svg >
3539 <span class =" lang-picker-label" >{ currentLabel } </span >
36- <span class =" lang-picker-caret" >▾ </span >
40+ <span class =" lang-picker-caret" aria-hidden = " true " >▾ </span >
3741 </button >
38- <div class =" lang-picker-menu" >
42+ <ul id = { menuId } class =" lang-picker-menu" hidden >
3943 { entries .map (([code , { label , path }]) => (
40- <a href = { localeHref (code , path )} data-lang = { code } class :list = { [' lang-picker-item' , code === locale && ' lang-current' ]} >
41- <span class = " lang-picker-item-label" >{ label } </span >
42- { code === locale && <span class = " lang-picker-check" >✓ </span >}
43- </a >
44+ <li >
45+ <a
46+ href = { localeHref (code , path )}
47+ data-lang = { code }
48+ lang = { code }
49+ dir = { isRtl (code ) ? ' rtl' : ' ltr' }
50+ aria-current = { code === locale ? ' page' : undefined }
51+ class :list = { [' lang-picker-item' , code === locale && ' lang-current' ]}
52+ >
53+ <span class = " lang-picker-item-label" >{ label } </span >
54+ { code === locale && <span class = " lang-picker-check" aria-hidden = " true" >✓ </span >}
55+ </a >
56+ </li >
4457 ))}
45- </div >
58+ </ul >
4659</div >
4760
4861<script is:inline >
@@ -54,31 +67,79 @@ function localeHref(code: string, basePath: string): string {
5467 */
5568(function() {
5669 document.querySelectorAll('.lang-picker-btn').forEach(function(btn) {
70+ var menu = document.getElementById(btn.getAttribute('aria-controls'));
71+ if (!menu) return;
72+
73+ function closeMenu(returnFocus) {
74+ menu.hidden = true;
75+ btn.setAttribute('aria-expanded', 'false');
76+ if (returnFocus) btn.focus();
77+ }
78+
79+ function openMenu(focusFirst) {
80+ document.querySelectorAll('.lang-picker-menu:not([hidden])').forEach(function(otherMenu) {
81+ otherMenu.hidden = true;
82+ });
83+ document.querySelectorAll('.lang-picker-btn').forEach(function(otherButton) {
84+ otherButton.setAttribute('aria-expanded', 'false');
85+ });
86+ menu.hidden = false;
87+ btn.setAttribute('aria-expanded', 'true');
88+ if (focusFirst) {
89+ var current = menu.querySelector('[aria-current="page"]');
90+ var first = menu.querySelector('.lang-picker-item');
91+ (current || first)?.focus();
92+ }
93+ }
94+
5795 btn.addEventListener('click', function(e) {
5896 e.stopPropagation();
59- var menu = btn.parentElement.querySelector('.lang-picker-menu');
60- var isOpen = menu.classList.contains('is-open');
61- // Close any other open pickers first
62- document.querySelectorAll('.lang-picker-menu.is-open').forEach(function(m) { m.classList.remove('is-open'); });
63- document.querySelectorAll('.lang-picker-btn').forEach(function(b) { b.setAttribute('aria-expanded', 'false'); });
64- if (!isOpen) {
65- menu.classList.add('is-open');
66- btn.setAttribute('aria-expanded', 'true');
97+ if (menu.hidden) openMenu(false);
98+ else closeMenu(false);
99+ });
100+
101+ btn.addEventListener('keydown', function(e) {
102+ if (e.key === 'ArrowDown') {
103+ e.preventDefault();
104+ openMenu(true);
105+ }
106+ });
107+
108+ menu.addEventListener('keydown', function(e) {
109+ var items = Array.from(menu.querySelectorAll('.lang-picker-item'));
110+ var index = items.indexOf(document.activeElement);
111+
112+ if (e.key === 'Escape') {
113+ e.preventDefault();
114+ closeMenu(true);
115+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
116+ e.preventDefault();
117+ var direction = e.key === 'ArrowDown' ? 1 : -1;
118+ items[(index + direction + items.length) % items.length]?.focus();
119+ } else if (e.key === 'Home' || e.key === 'End') {
120+ e.preventDefault();
121+ items[e.key === 'Home' ? 0 : items.length - 1]?.focus();
67122 }
68123 });
69124 });
70125
71126 document.addEventListener('click', function(e) {
72127 if (!e.target.closest('.lang-picker')) {
73- document.querySelectorAll('.lang-picker-menu.is-open ').forEach(function(m) { m.classList.remove('is-open') ; });
128+ document.querySelectorAll('.lang-picker-menu:not([hidden]) ').forEach(function(m) { m.hidden = true ; });
74129 document.querySelectorAll('.lang-picker-btn').forEach(function(b) { b.setAttribute('aria-expanded', 'false'); });
75130 }
76131 });
77132
78133 document.addEventListener('keydown', function(e) {
79134 if (e.key === 'Escape') {
80- document.querySelectorAll('.lang-picker-menu.is-open').forEach(function(m) { m.classList.remove('is-open'); });
81- document.querySelectorAll('.lang-picker-btn').forEach(function(b) { b.setAttribute('aria-expanded', 'false'); });
135+ var openMenu = document.querySelector('.lang-picker-menu:not([hidden])');
136+ if (!openMenu) return;
137+ openMenu.hidden = true;
138+ var button = document.querySelector('[aria-controls="' + openMenu.id + '"]');
139+ if (button) {
140+ button.setAttribute('aria-expanded', 'false');
141+ button.focus();
142+ }
82143 }
83144 });
84145
@@ -106,7 +167,8 @@ function localeHref(code: string, basePath: string): string {
106167
107168 if (match && match.getAttribute('data-lang') !== current) {
108169 match.classList.add('lang-detected');
109- menu.insertBefore(match, menu.firstChild);
170+ match.setAttribute('title', 'Suggested from browser language');
171+ menu.insertBefore(match.parentElement, menu.firstElementChild);
110172 }
111173 });
112174})();
0 commit comments