1515
1616package com.amplifyframework.ui.authenticator.ui
1717
18- import androidx.compose.foundation.background
1918import androidx.compose.foundation.clickable
20- import androidx.compose.foundation.layout.Box
19+ import androidx.compose.foundation.layout.Arrangement
20+ import androidx.compose.foundation.layout.Column
21+ import androidx.compose.foundation.layout.Row
22+ import androidx.compose.foundation.layout.fillMaxSize
2123import androidx.compose.foundation.layout.fillMaxWidth
2224import androidx.compose.foundation.layout.padding
23- import androidx.compose.foundation.layout.width
2425import androidx.compose.foundation.lazy.LazyColumn
25- import androidx.compose.foundation.lazy.items
26+ import androidx.compose.foundation.lazy.LazyItemScope
27+ import androidx.compose.foundation.lazy.itemsIndexed
2628import androidx.compose.foundation.lazy.rememberLazyListState
2729import androidx.compose.foundation.text.KeyboardActions
2830import androidx.compose.foundation.text.KeyboardOptions
2931import androidx.compose.material3.Divider
30- import androidx.compose.material3.MaterialTheme
32+ import androidx.compose.material3.ExperimentalMaterial3Api
33+ import androidx.compose.material3.Icon
34+ import androidx.compose.material3.IconButton
35+ import androidx.compose.material3.ModalBottomSheet
3136import androidx.compose.material3.OutlinedTextField
3237import androidx.compose.material3.Surface
3338import androidx.compose.material3.Text
39+ import androidx.compose.material3.rememberModalBottomSheetState
3440import androidx.compose.runtime.Composable
3541import androidx.compose.runtime.LaunchedEffect
3642import androidx.compose.runtime.Stable
3743import androidx.compose.runtime.derivedStateOf
3844import androidx.compose.runtime.getValue
3945import androidx.compose.runtime.mutableStateOf
46+ import androidx.compose.runtime.remember
47+ import androidx.compose.runtime.rememberCoroutineScope
4048import androidx.compose.runtime.saveable.listSaver
4149import androidx.compose.runtime.saveable.rememberSaveable
4250import androidx.compose.runtime.setValue
51+ import androidx.compose.ui.Alignment
4352import androidx.compose.ui.Modifier
4453import androidx.compose.ui.focus.FocusDirection
45- import androidx.compose.ui.graphics.Color
4654import androidx.compose.ui.platform.LocalFocusManager
55+ import androidx.compose.ui.res.painterResource
56+ import androidx.compose.ui.res.stringResource
57+ import androidx.compose.ui.semantics.contentDescription
58+ import androidx.compose.ui.semantics.semantics
4759import androidx.compose.ui.text.input.ImeAction
4860import androidx.compose.ui.text.input.KeyboardType
4961import androidx.compose.ui.unit.dp
50- import androidx.compose .ui.window.Dialog
62+ import com.amplifyframework .ui.authenticator.R
5163import com.amplifyframework.ui.authenticator.forms.FieldConfig
5264import com.amplifyframework.ui.authenticator.forms.MutableFieldState
5365import com.amplifyframework.ui.authenticator.strings.StringResolver
54- import com.amplifyframework.ui.authenticator.util.dialCodeFor
55- import com.amplifyframework.ui.authenticator.util.dialCodeList
66+ import com.amplifyframework.ui.authenticator.util.Region
67+ import com.amplifyframework.ui.authenticator.util.regionList
68+ import com.amplifyframework.ui.authenticator.util.regionMap
5669import java.util.Locale
70+ import kotlinx.coroutines.launch
5771
5872@Stable
5973private class PhoneNumberFieldState (
60- initialDialCode : String ,
74+ initialRegionCode : String ,
6175 initialNumber : String = " "
6276) {
63- var dialCode by mutableStateOf(initialDialCode )
77+ var region by mutableStateOf(regionMap[initialRegionCode] ? : regionMap[ " US " ] !! )
6478 var number by mutableStateOf(initialNumber)
6579 var expanded by mutableStateOf(false )
6680 val fieldValue by derivedStateOf {
67- if (number.isEmpty()) " " else dialCode + number
81+ if (number.isEmpty()) " " else region. dialCode + number
6882 }
6983}
7084
@@ -82,12 +96,12 @@ internal fun PhoneInputField(
8296
8397 val state = rememberSaveable(
8498 saver = listSaver(
85- save = { listOf (it.dialCode , it.number) },
99+ save = { listOf (it.region.code , it.number) },
86100 restore = { PhoneNumberFieldState (it[0 ], it[1 ]) }
87101 )
88102 ) {
89- val countryCode = Locale .getDefault().country
90- PhoneNumberFieldState (initialDialCode = dialCodeFor(countryCode) )
103+ val regionCode = Locale .getDefault().country
104+ PhoneNumberFieldState (initialRegionCode = regionCode )
91105 }
92106
93107 OutlinedTextField (
@@ -121,6 +135,7 @@ internal fun PhoneInputField(
121135 }
122136}
123137
138+ @OptIn(ExperimentalMaterial3Api ::class )
124139@Composable
125140private fun DialCodeSelector (
126141 state : PhoneNumberFieldState
@@ -129,53 +144,111 @@ private fun DialCodeSelector(
129144 modifier = Modifier
130145 .clickable { state.expanded = true }
131146 .padding(8 .dp),
132- text = state.dialCode
147+ text = state.region. dialCode
133148 )
134149
135150 if (state.expanded) {
136- Dialog (
151+ val scope = rememberCoroutineScope()
152+ val bottomSheetState = rememberModalBottomSheetState()
153+ ModalBottomSheet (
154+ sheetState = bottomSheetState,
137155 onDismissRequest = { state.expanded = false }
138156 ) {
139- val listState = rememberLazyListState(
140- initialFirstVisibleItemIndex = dialCodeList.indexOf(state.dialCode)
141- )
142- Box (
143- modifier = Modifier
144- .padding(vertical = 24 .dp)
145- .width(100 .dp)
146- ) {
147- Surface {
148- LazyColumn (
149- state = listState
150- ) {
151- items(dialCodeList) { dialCode ->
152- val color = if (dialCode == state.dialCode) {
153- MaterialTheme .colorScheme.onSecondaryContainer
154- } else {
155- MaterialTheme .colorScheme.onSurface
156- }
157- val background = if (dialCode == state.dialCode) {
158- MaterialTheme .colorScheme.secondaryContainer
159- } else {
160- Color .Transparent
161- }
162- Text (
163- modifier = Modifier
164- .fillMaxWidth()
165- .background(background)
166- .clickable {
167- state.dialCode = dialCode
168- state.expanded = false
157+ val listState = rememberLazyListState()
158+ var filterTerm by remember { mutableStateOf(" " ) }
159+ val displayedRegions by remember {
160+ derivedStateOf {
161+ if (filterTerm.isBlank()) {
162+ regionList
163+ } else {
164+ regionList.filter { it.name.contains(filterTerm, ignoreCase = true ) }
165+ }
166+ }
167+ }
168+ Surface (modifier = Modifier .fillMaxSize()) {
169+ Column (horizontalAlignment = Alignment .CenterHorizontally ) {
170+ RegionSearchBox (
171+ modifier = Modifier .fillMaxWidth().padding(bottom = 16 .dp).padding(horizontal = 16 .dp),
172+ value = filterTerm,
173+ onValueChange = { filterTerm = it }
174+ )
175+ LazyColumn (state = listState) {
176+ itemsIndexed(displayedRegions) { index, region ->
177+ RegionItem (
178+ showDivider = index < displayedRegions.lastIndex,
179+ region = region,
180+ onClick = {
181+ state.region = it
182+ scope.launch { bottomSheetState.hide() }.invokeOnCompletion {
183+ if (! bottomSheetState.isVisible) {
184+ state.expanded = false
185+ }
169186 }
170- .padding(8 .dp),
171- color = color,
172- text = dialCode
187+ }
173188 )
174- Divider ()
175189 }
176190 }
177191 }
178192 }
179193 }
180194 }
181195}
196+
197+ @Composable
198+ private fun RegionSearchBox (
199+ value : String ,
200+ onValueChange : (String ) -> Unit ,
201+ modifier : Modifier = Modifier
202+ ) {
203+ OutlinedTextField (
204+ modifier = modifier,
205+ value = value,
206+ onValueChange = onValueChange,
207+ leadingIcon = {
208+ Icon (
209+ painter = painterResource(R .drawable.ic_authenticator_search),
210+ contentDescription = null
211+ )
212+ },
213+ label = {
214+ Text (stringResource(R .string.amplify_ui_authenticator_field_phone_search))
215+ },
216+ trailingIcon = {
217+ if (value.isNotBlank()) {
218+ IconButton (onClick = { onValueChange(" " ) }) {
219+ Icon (
220+ painter = painterResource(R .drawable.ic_authenticator_clear),
221+ contentDescription = stringResource(R .string.amplify_ui_authenticator_field_phone_search_clear)
222+ )
223+ }
224+ }
225+ }
226+ )
227+ }
228+
229+ @Composable
230+ private fun LazyItemScope.RegionItem (
231+ showDivider : Boolean ,
232+ region : Region ,
233+ onClick : (Region ) -> Unit
234+ ) {
235+ Row (
236+ modifier = Modifier
237+ .fillParentMaxWidth()
238+ .clickable { onClick(region) }
239+ .padding(vertical = 12 .dp, horizontal = 16 .dp),
240+ horizontalArrangement = Arrangement .SpaceBetween
241+ ) {
242+ Text (
243+ text = " ${region.flagEmoji} ${region.name} " ,
244+ modifier = Modifier .semantics {
245+ contentDescription = region.name // Don't read the flag emoji when using talkback
246+ }
247+ )
248+ Text (region.dialCode)
249+ }
250+
251+ if (showDivider) {
252+ Divider (modifier = Modifier .padding(horizontal = 16 .dp))
253+ }
254+ }
0 commit comments