1
+ /**
2
+ * Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
3
+ */
4
+
1
5
import { useRef , useEffect , useState , useMemo } from "react" ;
6
+ import { useNavigate } from "react-router-dom" ;
2
7
3
8
import { useAppContext } from "@contexts/AppContext" ;
4
9
import { useKeyboardNavigation } from "@hooks/useKeyboardNavigation" ;
5
10
import { useLanguages } from "@hooks/useLanguages" ;
6
11
import { LanguageType } from "@types" ;
12
+ import { configureUserSelection } from "@utils/configureUserSelection" ;
13
+ import {
14
+ getLanguageDisplayLogo ,
15
+ getLanguageDisplayName ,
16
+ } from "@utils/languageUtils" ;
17
+ import { slugify } from "@utils/slugify" ;
7
18
8
19
import SubLanguageSelector from "./SubLanguageSelector" ;
9
20
10
- // Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
11
-
12
21
const LanguageSelector = ( ) => {
13
- const { language, setLanguage } = useAppContext ( ) ;
22
+ const navigate = useNavigate ( ) ;
23
+
24
+ const { language, subLanguage, setSearchText } = useAppContext ( ) ;
14
25
const { fetchedLanguages, loading, error } = useLanguages ( ) ;
15
- const allLanguages = useMemo (
16
- ( ) =>
17
- fetchedLanguages . flatMap ( ( lang ) =>
18
- lang . subLanguages . length > 0
19
- ? [
20
- lang ,
21
- ...lang . subLanguages . map ( ( subLang ) => ( {
22
- ...subLang ,
23
- mainLanguage : lang ,
24
- subLanguages : [ ] ,
25
- } ) ) ,
26
- ]
27
- : [ lang ]
28
- ) ,
29
- [ fetchedLanguages ]
30
- ) ;
31
26
32
27
const dropdownRef = useRef < HTMLDivElement > ( null ) ;
33
- const [ isOpen , setIsOpen ] = useState ( false ) ;
28
+ const [ isOpen , setIsOpen ] = useState < boolean > ( false ) ;
34
29
const [ openedLanguages , setOpenedLanguages ] = useState < LanguageType [ ] > ( [ ] ) ;
35
30
36
- const handleSelect = ( selected : LanguageType ) => {
37
- setLanguage ( selected ) ;
31
+ const keyboardItems = useMemo ( ( ) => {
32
+ return fetchedLanguages . flatMap ( ( lang ) =>
33
+ openedLanguages . map ( ( ol ) => ol . name ) . includes ( lang . name )
34
+ ? [
35
+ { languageName : lang . name } ,
36
+ ...lang . subLanguages . map ( ( sl ) => ( {
37
+ languageName : lang . name ,
38
+ subLanguageName : sl . name ,
39
+ } ) ) ,
40
+ ]
41
+ : [ { languageName : lang . name } ]
42
+ ) ;
43
+ } , [ fetchedLanguages , openedLanguages ] ) ;
44
+
45
+ const displayName = useMemo (
46
+ ( ) => getLanguageDisplayName ( language . name , subLanguage ) ,
47
+ [ language . name , subLanguage ]
48
+ ) ;
49
+
50
+ const displayLogo = useMemo (
51
+ ( ) => getLanguageDisplayLogo ( language . name , subLanguage ) ,
52
+ [ language . name , subLanguage ]
53
+ ) ;
54
+
55
+ const handleToggleSubLanguage = ( name : LanguageType [ "name" ] ) => {
56
+ const isAlreadyOpened = openedLanguages . some ( ( lang ) => lang . name === name ) ;
57
+ const openedLang = fetchedLanguages . find ( ( lang ) => lang . name === name ) ;
58
+ if ( openedLang === undefined || openedLang . subLanguages . length === 0 ) {
59
+ return ;
60
+ }
61
+
62
+ if ( ! isAlreadyOpened ) {
63
+ setOpenedLanguages ( ( prev ) => [ ...prev , openedLang ] ) ;
64
+ } else {
65
+ setOpenedLanguages ( ( prev ) =>
66
+ prev . filter ( ( lang ) => lang . name !== openedLang . name )
67
+ ) ;
68
+ }
69
+ } ;
70
+
71
+ /**
72
+ * When setting a new language we need to ensure that a category
73
+ * has been set given this new language.
74
+ * Ensure that the search text is cleared.
75
+ */
76
+ const handleSelect = async ( selected : LanguageType ) => {
77
+ const {
78
+ language : newLanguage ,
79
+ subLanguage : newSubLanguage ,
80
+ category : newCategory ,
81
+ } = await configureUserSelection ( {
82
+ languageName : selected . name ,
83
+ } ) ;
84
+
85
+ setSearchText ( "" ) ;
86
+ navigate (
87
+ `/${ slugify ( newLanguage . name ) } /${ slugify ( newSubLanguage ) } /${ slugify ( newCategory ) } `
88
+ ) ;
38
89
setIsOpen ( false ) ;
39
90
setOpenedLanguages ( [ ] ) ;
40
91
} ;
41
92
93
+ const afterSelect = ( ) => {
94
+ setIsOpen ( false ) ;
95
+ } ;
96
+
97
+ const handleSubLanguageSelect = async (
98
+ selectedLanguageName : LanguageType [ "name" ] ,
99
+ selectedSubLanguageName :
100
+ | LanguageType [ "subLanguages" ] [ number ] [ "name" ]
101
+ | undefined
102
+ ) => {
103
+ const {
104
+ language : newLanguage ,
105
+ subLanguage : newSubLanguage ,
106
+ category : newCategory ,
107
+ } = await configureUserSelection ( {
108
+ languageName : selectedLanguageName ,
109
+ subLanguageName : selectedSubLanguageName ,
110
+ } ) ;
111
+
112
+ setSearchText ( "" ) ;
113
+ navigate (
114
+ `/${ slugify ( newLanguage . name ) } /${ slugify ( newSubLanguage ) } /${ slugify ( newCategory ) } `
115
+ ) ;
116
+ afterSelect ( ) ;
117
+ } ;
118
+
42
119
const { focusedIndex, handleKeyDown, resetFocus, focusFirst } =
43
120
useKeyboardNavigation ( {
44
- items : allLanguages ,
121
+ items : keyboardItems ,
45
122
isOpen,
46
- openedLanguages,
47
- toggleDropdown : ( openedLang ) => handleToggleSublanguage ( openedLang ) ,
48
- onSelect : handleSelect ,
123
+ toggleDropdown : ( l ) => handleToggleSubLanguage ( l ) ,
124
+ onSelect : ( l , sl ) => handleSubLanguageSelect ( l , sl ) ,
49
125
onClose : ( ) => setIsOpen ( false ) ,
50
126
} ) ;
51
127
@@ -60,20 +136,6 @@ const LanguageSelector = () => {
60
136
} , 0 ) ;
61
137
} ;
62
138
63
- const handleToggleSublanguage = ( openedLang : LanguageType ) => {
64
- const isAlreadyOpened = openedLanguages . some (
65
- ( lang ) => lang . name === openedLang . name
66
- ) ;
67
-
68
- if ( ! isAlreadyOpened ) {
69
- setOpenedLanguages ( ( prev ) => [ ...prev , openedLang ] ) ;
70
- } else {
71
- setOpenedLanguages ( ( prev ) =>
72
- prev . filter ( ( lang ) => lang . name !== openedLang . name )
73
- ) ;
74
- }
75
- } ;
76
-
77
139
const toggleDropdown = ( ) => {
78
140
setIsOpen ( ( prev ) => {
79
141
if ( ! prev ) setTimeout ( focusFirst , 0 ) ;
@@ -88,13 +150,6 @@ const LanguageSelector = () => {
88
150
// eslint-disable-next-line react-hooks/exhaustive-deps
89
151
} , [ isOpen ] ) ;
90
152
91
- useEffect ( ( ) => {
92
- if ( language . mainLanguage ) {
93
- handleToggleSublanguage ( language . mainLanguage ) ;
94
- }
95
- // eslint-disable-next-line react-hooks/exhaustive-deps
96
- } , [ language ] ) ;
97
-
98
153
useEffect ( ( ) => {
99
154
if ( isOpen && focusedIndex >= 0 ) {
100
155
const element = document . querySelector (
@@ -104,8 +159,13 @@ const LanguageSelector = () => {
104
159
}
105
160
} , [ isOpen , focusedIndex ] ) ;
106
161
107
- if ( loading ) return < p > Loading languages...</ p > ;
108
- if ( error ) return < p > Error fetching languages: { error } </ p > ;
162
+ if ( loading ) {
163
+ return < p > Loading languages...</ p > ;
164
+ }
165
+
166
+ if ( error ) {
167
+ return < p > Error fetching languages: { error } </ p > ;
168
+ }
109
169
110
170
return (
111
171
< div
@@ -121,8 +181,8 @@ const LanguageSelector = () => {
121
181
onClick = { toggleDropdown }
122
182
>
123
183
< div className = "selector__value" >
124
- < img src = { language . icon } alt = "" />
125
- < span > { language . name || "Select a language" } </ span >
184
+ < img src = { displayLogo } alt = "" />
185
+ < span > { displayName } </ span >
126
186
</ div >
127
187
< span className = "selector__arrow" />
128
188
</ button >
@@ -136,13 +196,12 @@ const LanguageSelector = () => {
136
196
{ fetchedLanguages . map ( ( lang , index ) =>
137
197
lang . subLanguages . length > 0 ? (
138
198
< SubLanguageSelector
139
- key = { index }
140
- mainLanguage = { lang }
141
- afterSelect = { ( ) => {
142
- setIsOpen ( false ) ;
143
- } }
199
+ key = { lang . name }
144
200
opened = { openedLanguages . includes ( lang ) }
145
- onDropdownToggle = { handleToggleSublanguage }
201
+ parentLanguage = { lang }
202
+ onDropdownToggle = { handleToggleSubLanguage }
203
+ handleParentSelect = { handleSelect }
204
+ afterSelect = { afterSelect }
146
205
/>
147
206
) : (
148
207
< li
0 commit comments