22//
33// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
44
5+ import { Composite , CompositeItem } from "@floating-ui/react" ;
56import { PublicIcon } from "@vector-im/compound-design-tokens/assets/web/icons" ;
67import { Button , Tooltip } from "@vector-im/compound-web" ;
7- import { forwardRef , useMemo , useState , useTransition } from "react" ;
8+ import { forwardRef , useCallback , useMemo , useTransition } from "react" ;
89import { FormattedMessage , useIntl } from "react-intl" ;
910
1011import * as Dialog from "@/components/dialog" ;
1112import { AVAILABLE_LOCALES , useBestLocale } from "@/intl" ;
12- import * as messages from "@/messages" ;
1313import { useLocaleStore } from "@/stores/locale" ;
1414
1515import styles from "./language-switcher.module.css" ;
@@ -59,30 +59,33 @@ const LanguageName: React.FC<LanguageNameProps> = ({
5959 return displayNameFormatter . of ( locale ) ;
6060} ;
6161
62+ const CHOICES = [ null , ...AVAILABLE_LOCALES ] ;
63+
6264export const LanguageSwitcher : React . FC = ( ) => {
63- const [ open , setOpen ] = useState ( false ) ;
65+ const intl = useIntl ( ) ;
6466 const [ pending , startTransition ] = useTransition ( ) ;
6567 const { selectedLocale, setLocale, clearLocale } = useLocaleStore ( ) ;
6668 const bestLocale = useBestLocale ( ) ;
6769
68- const handleLanguageSelect = ( locale : string ) => {
69- if ( locale !== selectedLocale ) {
70- startTransition ( ( ) => setLocale ( locale ) ) ;
71- }
72- } ;
70+ const selectItem = useCallback (
71+ ( index : number ) => {
72+ const locale = CHOICES [ index ] ;
73+ if ( locale === selectedLocale ) return ;
74+ startTransition ( ( ) => {
75+ if ( locale ) setLocale ( locale ) ;
76+ else clearLocale ( ) ;
77+ } ) ;
78+ } ,
79+ [ setLocale , clearLocale , selectedLocale ] ,
80+ ) ;
7381
74- const handleClearSelection = ( ) => {
75- if ( selectedLocale !== null ) {
76- startTransition ( ( ) => clearLocale ( ) ) ;
77- }
78- } ;
82+ const activeIndex = useMemo ( ( ) => {
83+ const index = CHOICES . indexOf ( selectedLocale ) ;
84+ return index === - 1 ? 0 : index ;
85+ } , [ selectedLocale ] ) ;
7986
8087 return (
81- < Dialog . Root
82- open = { open }
83- onOpenChange = { setOpen }
84- trigger = { < LanguageSwitcherButton /> }
85- >
88+ < Dialog . Root trigger = { < LanguageSwitcherButton /> } >
8689 < Dialog . Title >
8790 < FormattedMessage
8891 id = "ui.language_switcher.title"
@@ -99,41 +102,42 @@ export const LanguageSwitcher: React.FC = () => {
99102 />
100103 </ Dialog . Description >
101104
102- < div className = { styles [ "language-row-list" ] } data-pending = { pending } >
103- < button
104- type = "button"
105- className = { styles [ "language-row" ] }
106- data-selected = { selectedLocale === null }
107- onClick = { handleClearSelection }
108- >
109- < FormattedMessage
110- id = "ui.language_switcher.browser_default"
111- defaultMessage = "Use browser settings ({language})"
112- description = "Option to use browser's default language in the language switcher"
113- values = { {
114- language : < LanguageName locale = { bestLocale } /> ,
115- } }
116- />
117- </ button >
118-
119- { AVAILABLE_LOCALES . map ( ( locale ) => (
120- < button
105+ < Composite
106+ orientation = "vertical"
107+ activeIndex = { activeIndex }
108+ onNavigate = { selectItem }
109+ role = "listbox"
110+ className = { styles [ "language-row-list" ] }
111+ aria-busy = { pending }
112+ aria-label = { intl . formatMessage ( {
113+ id : "ui.language_switcher.language_list_label" ,
114+ defaultMessage : "List of available languages" ,
115+ description :
116+ "Aria label for the list of available languages in the language switcher" ,
117+ } ) }
118+ >
119+ { CHOICES . map ( ( locale , index ) => (
120+ < CompositeItem
121121 key = { locale }
122- type = "button"
122+ role = "option"
123+ aria-selected = { index === activeIndex }
123124 className = { styles [ "language-row" ] }
124- data-selected = { selectedLocale === locale }
125- onClick = { ( ) => handleLanguageSelect ( locale ) }
126125 >
127- < LanguageName locale = { locale } />
128- </ button >
126+ { locale === null ? (
127+ < FormattedMessage
128+ id = "ui.language_switcher.browser_default"
129+ defaultMessage = "Use browser settings ({language})"
130+ description = "Option to use browser's default language in the language switcher"
131+ values = { {
132+ language : < LanguageName locale = { bestLocale } /> ,
133+ } }
134+ />
135+ ) : (
136+ < LanguageName locale = { locale } />
137+ ) }
138+ </ CompositeItem >
129139 ) ) }
130- </ div >
131-
132- < Dialog . Close asChild >
133- < Button type = "button" kind = "tertiary" >
134- < FormattedMessage { ...messages . actionClose } />
135- </ Button >
136- </ Dialog . Close >
140+ </ Composite >
137141 </ Dialog . Root >
138142 ) ;
139143} ;
0 commit comments