Skip to content

Commit ce661c0

Browse files
authored
feat(authenticator): Update dial code selector to a bottom sheet with search capability (#32)
1 parent f465f13 commit ce661c0

File tree

6 files changed

+421
-523
lines changed

6 files changed

+421
-523
lines changed

authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/PhoneInputField.kt

Lines changed: 124 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,56 +15,70 @@
1515

1616
package com.amplifyframework.ui.authenticator.ui
1717

18-
import androidx.compose.foundation.background
1918
import 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
2123
import androidx.compose.foundation.layout.fillMaxWidth
2224
import androidx.compose.foundation.layout.padding
23-
import androidx.compose.foundation.layout.width
2425
import 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
2628
import androidx.compose.foundation.lazy.rememberLazyListState
2729
import androidx.compose.foundation.text.KeyboardActions
2830
import androidx.compose.foundation.text.KeyboardOptions
2931
import 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
3136
import androidx.compose.material3.OutlinedTextField
3237
import androidx.compose.material3.Surface
3338
import androidx.compose.material3.Text
39+
import androidx.compose.material3.rememberModalBottomSheetState
3440
import androidx.compose.runtime.Composable
3541
import androidx.compose.runtime.LaunchedEffect
3642
import androidx.compose.runtime.Stable
3743
import androidx.compose.runtime.derivedStateOf
3844
import androidx.compose.runtime.getValue
3945
import androidx.compose.runtime.mutableStateOf
46+
import androidx.compose.runtime.remember
47+
import androidx.compose.runtime.rememberCoroutineScope
4048
import androidx.compose.runtime.saveable.listSaver
4149
import androidx.compose.runtime.saveable.rememberSaveable
4250
import androidx.compose.runtime.setValue
51+
import androidx.compose.ui.Alignment
4352
import androidx.compose.ui.Modifier
4453
import androidx.compose.ui.focus.FocusDirection
45-
import androidx.compose.ui.graphics.Color
4654
import 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
4759
import androidx.compose.ui.text.input.ImeAction
4860
import androidx.compose.ui.text.input.KeyboardType
4961
import androidx.compose.ui.unit.dp
50-
import androidx.compose.ui.window.Dialog
62+
import com.amplifyframework.ui.authenticator.R
5163
import com.amplifyframework.ui.authenticator.forms.FieldConfig
5264
import com.amplifyframework.ui.authenticator.forms.MutableFieldState
5365
import 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
5669
import java.util.Locale
70+
import kotlinx.coroutines.launch
5771

5872
@Stable
5973
private 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
125140
private 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

Comments
 (0)