Skip to content

Commit

Permalink
refactor: Clean components
Browse files Browse the repository at this point in the history
  • Loading branch information
FabianDevel committed Dec 17, 2024
1 parent 9e9696b commit fd276d3
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,29 @@ fun SwissTransferInputChip(modifier: Modifier = Modifier, text: String, onDismis

val focusManager = LocalFocusManager.current

fun onKeyEvent(event: KeyEvent): Boolean {
if (event.type != KeyEventType.KeyUp) return false

return when {
isDirectionalKey(event.key) && isFocused -> {
isSelected = true
true
}
event.key == Key.Backspace || event.key == Key.Delete -> {
when {
isSelected -> onDismiss()
isFocused -> isSelected = true
}
true
}
event.key == Key.NavigateOut -> {
focusManager.moveFocus(FocusDirection.Exit)
true
}
else -> false
}
}

InputChip(
modifier = modifier
.focusRequester(focusRequester)
Expand All @@ -79,28 +102,7 @@ fun SwissTransferInputChip(modifier: Modifier = Modifier, text: String, onDismis
isFocused = focusState.isFocused || focusState.hasFocus
if (!isFocused) isSelected = false
}
.onKeyEvent { event ->
if (event.type != KeyEventType.KeyUp) return@onKeyEvent false

when {
isDirectionalKey(event.key) && isFocused -> {
isSelected = true
true
}
event.key == Key.Backspace || event.key == Key.Delete -> {
when {
isSelected -> onDismiss()
isFocused -> isSelected = true
}
true
}
event.key == Key.NavigateOut -> {
focusManager.moveFocus(FocusDirection.Exit)
true
}
else -> false
}
}
.onKeyEvent { event -> onKeyEvent(event) }
.focusable(),
selected = isFocused,
onClick = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
Expand Down Expand Up @@ -59,7 +56,6 @@ import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark

const val EMAIL_FIELD_TAG = "EmailAddressTextField"

@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable
fun EmailAddressTextField(
modifier: Modifier = Modifier,
Expand All @@ -71,8 +67,6 @@ fun EmailAddressTextField(
supportingText: (@Composable () -> Unit)? = null,
) {

val focusManager = LocalFocusManager.current

var text by rememberSaveable { mutableStateOf(initialValue) }
var currentFocus: Focus? by remember { mutableStateOf(null) }
val interactionSource = remember { MutableInteractionSource() }
Expand All @@ -81,11 +75,7 @@ fun EmailAddressTextField(
val isFocused by interactionSource.collectIsFocusedAsState()

val cursorColor by animateColorAsState(
targetValue = if (isError) {
SwissTransferTheme.materialColors.error
} else {
SwissTransferTheme.materialColors.primary
},
targetValue = if (isError) SwissTransferTheme.materialColors.error else SwissTransferTheme.materialColors.primary,
label = "CursorColor",
)

Expand All @@ -102,109 +92,156 @@ fun EmailAddressTextField(
onValueChange(newValue)
}

fun onKeyEvent(event: KeyEvent): Boolean {
val shouldFocusLastChip = isFocused && validatedEmails.get().isNotEmpty()
if (event.type == KeyEventType.KeyDown && event.key == Key.Backspace && shouldFocusLastChip) {
runCatching {
lastChipFocusRequester.requestFocus()
}.onFailure {
SentryLog.e(EMAIL_FIELD_TAG, "The focusRequested wasn't registered with a non empty Chip list", it)
}

return true
}

return false
}

val keyboardActions = KeyboardActions(
onDone = {
val trimmedText = text.trim()
if (trimmedText.isEmail()) {
validatedEmails.set(validatedEmails.get() + trimmedText)
updateUiTextValue("")
}
},
)

val keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done,
showKeyboardOnFocus = true,
)

val emailAddressTextFieldModifier = modifier
.onFocusChanged { focusState ->
if (focusState.isFocused) {
val newFocus = Focus()
if (interactionSource.tryEmit(newFocus)) currentFocus = newFocus
} else {
currentFocus?.let { interactionSource.tryEmit(FocusInteraction.Unfocus(it)) }
}
}
.fillMaxWidth()
.onPreviewKeyEvent(::onKeyEvent)

BasicTextField(
modifier = emailAddressTextFieldModifier,
value = text,
onValueChange = ::updateUiTextValue,
modifier = modifier
.onFocusChanged { focusState ->
if (focusState.isFocused) {
val newFocus = Focus()
if (interactionSource.tryEmit(newFocus)) currentFocus = newFocus
} else {
currentFocus?.let { interactionSource.tryEmit(FocusInteraction.Unfocus(it)) }
}
}
.fillMaxWidth()
.onPreviewKeyEvent { event ->
val shouldFocusLastChip = isFocused && validatedEmails
.get()
.isNotEmpty()
if (event.type == KeyEventType.KeyDown && event.key == Key.Backspace && shouldFocusLastChip) {
runCatching {
lastChipFocusRequester.requestFocus()
}.onFailure {
SentryLog.e(EMAIL_FIELD_TAG, "The focusRequested wasn't registered with a non empty Chip list", it)
}

return@onPreviewKeyEvent true
}
false
},
textStyle = TextStyle(color = SwissTransferTheme.colors.primaryTextColor),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done,
showKeyboardOnFocus = true,
),
keyboardActions = KeyboardActions(
onDone = {
val trimmedText = text.trim()
if (trimmedText.isEmail()) {
validatedEmails.set(validatedEmails.get() + trimmedText)
updateUiTextValue("")
}
},
),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = true,
cursorBrush = SolidColor(cursorColor),
decorationBox = { innerTextField ->
OutlinedTextFieldDefaults.DecorationBox(
value = text,
innerTextField = {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(Margin.Mini),
itemVerticalAlignment = Alignment.CenterVertically,
) {
validatedEmails.get().forEach { email ->
val chipModifier = if (email == validatedEmails.get().lastOrNull()) {
Modifier.focusRequester(lastChipFocusRequester)
} else {
Modifier
}
SwissTransferInputChip(
modifier = chipModifier,
text = email,
onDismiss = {
validatedEmails.set(validatedEmails.get().minus(email))
focusManager.moveFocus(FocusDirection.Exit)
},
)
}
Box(
modifier = Modifier
.widthIn(min = 80.dp)
.weight(1f)
) {
innerTextField()
}
}
},
enabled = true,
singleLine = true,
visualTransformation = if (validatedEmails.get().isNotEmpty() && !isFocused) {
// TODO: Remove this hack to make the label always in "above" position when the labelPosition will be
// available in the DecorationBox's API
VisualTransformation { TransformedText(AnnotatedString(label), OffsetMapping.Identity) }
} else {
VisualTransformation.None
},
EmailAddressDecorationBox(
text = text,
validatedEmails = validatedEmails,
lastChipFocusRequester = lastChipFocusRequester,
innerTextField = innerTextField,
isFocused = isFocused,
label = label,
interactionSource = interactionSource,
isError = isError,
supportingText = supportingText,
label = { Text(label) },
colors = textFieldColors,
) {
OutlinedTextFieldDefaults.Container(
enabled = true,
isError = isError,
interactionSource = interactionSource,
colors = textFieldColors,
)
}
textFieldColors = textFieldColors,
)
}
)
}

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun EmailAddressDecorationBox(
text: String,
validatedEmails: GetSetCallbacks<Set<String>>,
lastChipFocusRequester: FocusRequester,
innerTextField: @Composable () -> Unit,
isFocused: Boolean,
label: String,
interactionSource: MutableInteractionSource,
isError: Boolean,
supportingText: @Composable (() -> Unit)?,
textFieldColors: TextFieldColors,
) {
OutlinedTextFieldDefaults.DecorationBox(
value = text,
innerTextField = { EmailChipsAndInnerTextField(validatedEmails, lastChipFocusRequester, innerTextField) },
enabled = true,
singleLine = true,
visualTransformation = if (validatedEmails.get().isNotEmpty() && !isFocused) {
// TODO: Remove this hack to make the label always in "above" position when the labelPosition will be
// available in the DecorationBox's API
VisualTransformation { TransformedText(AnnotatedString(label), OffsetMapping.Identity) }
} else {
VisualTransformation.None
},
interactionSource = interactionSource,
isError = isError,
supportingText = supportingText,
label = { Text(label) },
colors = textFieldColors,
) {
OutlinedTextFieldDefaults.Container(
enabled = true,
isError = isError,
interactionSource = interactionSource,
colors = textFieldColors,
)
}
}

@Composable
@OptIn(ExperimentalLayoutApi::class)
private fun EmailChipsAndInnerTextField(
validatedEmails: GetSetCallbacks<Set<String>>,
lastChipFocusRequester: FocusRequester,
innerTextField: @Composable () -> Unit,
) {
val focusManager = LocalFocusManager.current

FlowRow(
horizontalArrangement = Arrangement.spacedBy(Margin.Mini),
itemVerticalAlignment = Alignment.CenterVertically,
) {
validatedEmails.get().forEach { email ->
val chipModifier = if (email == validatedEmails.get().lastOrNull()) {
Modifier.focusRequester(lastChipFocusRequester)
} else {
Modifier
}

SwissTransferInputChip(
modifier = chipModifier,
text = email,
onDismiss = {
validatedEmails.set(validatedEmails.get().minus(email))
focusManager.moveFocus(FocusDirection.Exit)
},
)
}

Box(
modifier = Modifier
.widthIn(min = 80.dp)
.weight(1f)
) {
innerTextField()
}
}
}

@PreviewLightAndDark
@Composable
private fun Preview(@PreviewParameter(EmailsPreviewParameter::class) emails: List<String>) {
Expand Down

0 comments on commit fd276d3

Please sign in to comment.