Skip to content

Commit a89d9e2

Browse files
committed
Refactor code for clarity
1 parent 5735c46 commit a89d9e2

1 file changed

Lines changed: 155 additions & 119 deletions

File tree

core/src/main/java/com/orange/ouds/core/component/OudsPinCodeInput.kt

Lines changed: 155 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ import androidx.compose.foundation.text.BasicSecureTextField
2626
import androidx.compose.foundation.text.KeyboardOptions
2727
import androidx.compose.foundation.text.input.InputTransformation
2828
import androidx.compose.foundation.text.input.KeyboardActionHandler
29+
import androidx.compose.foundation.text.input.TextFieldState
2930
import androidx.compose.foundation.text.input.delete
3031
import androidx.compose.foundation.text.input.forEachChangeReversed
3132
import androidx.compose.foundation.text.input.insert
3233
import androidx.compose.foundation.text.input.rememberTextFieldState
33-
import androidx.compose.foundation.text.input.then
3434
import androidx.compose.material3.ExperimentalMaterial3Api
3535
import androidx.compose.material3.LocalRippleConfiguration
3636
import androidx.compose.material3.RichTooltip
@@ -123,8 +123,6 @@ fun OudsPinCodeInput(
123123
interactionSource: MutableInteractionSource? = null
124124
) {
125125
@Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
126-
val interactionState by interactionSource.collectInteractionStateAsState()
127-
val pinCodeInputTokens = OudsTheme.componentsTokens.pinCodeInput
128126
val paddedValue = value.take(length.value).padEnd(length.value, OudsDigitInputPlaceholder)
129127
val textFieldState = rememberTextFieldState(
130128
initialText = paddedValue,
@@ -158,138 +156,176 @@ fun OudsPinCodeInput(
158156
state = textFieldState,
159157
keyboardOptions = KeyboardOptions(autoCorrectEnabled = false, keyboardType = KeyboardType.Number),
160158
onKeyboardAction = onKeyboardAction,
161-
inputTransformation = InputTransformation.then {
162-
changes.forEachChangeReversed { range, originalRange ->
163-
// Insertion
164-
if (range.length > 0) {
165-
val pasting = range.length > 1
166-
val baseText = if (pasting) OudsDigitInputPlaceholder.toString().repeat(length.value) else originalText.toString()
167-
// Retrieve added text
168-
val addedText = asCharSequence().substring(range)
169-
// Roll back to the original text or placeholders if pasting
170-
delete(0, this.length)
171-
insert(0, baseText)
172-
// Replace the base text with the added text
173-
// When pasting (i.e. range.length > 1), the base text is replaced from the start
174-
val start = if (pasting) 0 else range.min - 1
175-
val end = if (pasting) addedText.length else range.max - 1
176-
replace(start.coerceIn(0, length.value), end.coerceIn(0, length.value), addedText)
177-
placeCursorAfterCharAt(end.coerceIn(0, length.value - 1))
178-
}
179-
// Deletion
180-
else {
181-
// Insert placeholder digits to replace the deleted text
182-
val padText = OudsDigitInputPlaceholder.toString().repeat(originalRange.length)
183-
insert(originalRange.start, padText)
184-
// Get the first character of the deleted text
185-
// If that character was not a digit (i.e. a placeholder was displayed in the digit input)
186-
// then replace the previous character with a placeholder
187-
val firstDeletedChar = originalText[originalRange.start]
188-
val previousChar = asCharSequence().getOrNull(range.start - 1)
189-
if (!firstDeletedChar.isDigit() && previousChar != null) {
190-
replace(range.start - 1, range.start, OudsDigitInputPlaceholder.toString())
191-
placeCursorAfterCharAt(range.start - 1)
192-
}
193-
}
194-
}
195-
},
159+
inputTransformation = inputTransformation(length),
196160
interactionSource = interactionSource,
197161
decorator = {
162+
OudsPinCodeInputTooltipBox(
163+
textFieldState = textFieldState,
164+
length = length
165+
) {
166+
OudsPinCodeInputDecorator(
167+
textFieldState = textFieldState,
168+
length = length,
169+
outlined = outlined,
170+
error = error,
171+
helperText = helperText,
172+
onDigitClick = {
173+
focusRequester.requestFocus()
174+
// If keyboard is dismissed using the Android back key, the keyboard won't reappear when digit is clicked
175+
keyboardController?.show()
176+
},
177+
interactionSource = interactionSource
178+
)
179+
}
180+
}
181+
)
182+
}
183+
184+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
185+
@Composable
186+
private fun OudsPinCodeInputTooltipBox(textFieldState: TextFieldState, length: OudsPinCodeInputLength, content: @Composable () -> Unit) {
187+
val tooltipState = rememberTooltipState(isPersistent = true)
188+
TooltipBox(
189+
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
190+
positioning = TooltipAnchorPosition.Above,
191+
spacingBetweenTooltipAndAnchor = OudsTheme.spaces.fixed.extraSmall
192+
),
193+
tooltip = {
198194
val clipboard = LocalClipboard.current
199-
val tooltipState = rememberTooltipState(isPersistent = true)
200195
val scope = rememberCoroutineScope()
201-
202-
TooltipBox(
203-
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
204-
positioning = TooltipAnchorPosition.Above,
205-
spacingBetweenTooltipAndAnchor = OudsTheme.spaces.fixed.extraSmall
206-
),
207-
tooltip = {
208-
RichTooltip {
209-
CompositionLocalProvider(LocalRippleConfiguration provides null) {
210-
TextButton(
211-
onClick = {
212-
scope.launch {
213-
clipboard.getClipEntry()?.clipData?.let { clipData ->
214-
if (clipData.itemCount > 0) {
215-
val text = clipData.getItemAt(0).text.toString()
216-
textFieldState.edit {
217-
delete(0, this.length)
218-
val paddedText = text.padEnd(length.value, OudsDigitInputPlaceholder)
219-
insert(0, paddedText)
220-
replace(0, text.length.coerceIn(0, length.value), text)
221-
placeCursorAfterCharAt(text.length.coerceIn(0, length.value - 1))
222-
}
196+
RichTooltip {
197+
CompositionLocalProvider(LocalRippleConfiguration provides null) {
198+
TextButton(
199+
onClick = {
200+
scope.launch {
201+
clipboard.getClipEntry()?.clipData?.let { clipData ->
202+
if (clipData.itemCount > 0) {
203+
val text = clipData.getItemAt(0).text.toString()
204+
textFieldState.edit {
205+
insert(selection.min, text)
206+
with(inputTransformation(length)) {
207+
transformInput()
223208
}
224209
}
225-
tooltipState.dismiss()
226210
}
227211
}
228-
) {
229-
Text(text = stringResource(R.string.core_pinCodeInput_paste_label))
212+
tooltipState.dismiss()
230213
}
231214
}
215+
) {
216+
Text(text = stringResource(R.string.core_pinCodeInput_paste_label))
232217
}
233-
},
234-
state = tooltipState
218+
}
219+
}
220+
},
221+
state = tooltipState,
222+
content = content
223+
)
224+
}
225+
226+
@Composable
227+
fun OudsPinCodeInputDecorator(
228+
textFieldState: TextFieldState,
229+
length: OudsPinCodeInputLength,
230+
outlined: Boolean,
231+
error: OudsError?,
232+
helperText: String?,
233+
onDigitClick: (Int) -> Unit,
234+
interactionSource: MutableInteractionSource
235+
) {
236+
val interactionState by interactionSource.collectInteractionStateAsState()
237+
val pinCodeInputTokens = OudsTheme.componentsTokens.pinCodeInput
238+
BoxWithConstraints(contentAlignment = Alignment.Center) {
239+
val horizontalSpace = if (length == OudsPinCodeInputLength.Eight) 6.dp else pinCodeInputTokens.spaceColumnGapDigitInput.value
240+
val totalSpace = horizontalSpace * (length.value - 1)
241+
val digitWidth = (maxWidth - totalSpace) / length.value
242+
ConstraintLayout {
243+
val (row, helperTextErrorMessage) = createRefs()
244+
Row(
245+
modifier = Modifier
246+
.constrainAs(row) {
247+
top.linkTo(parent.top)
248+
start.linkTo(parent.start)
249+
end.linkTo(parent.end)
250+
},
251+
horizontalArrangement = Arrangement.spacedBy(horizontalSpace)
235252
) {
236-
BoxWithConstraints(contentAlignment = Alignment.Center) {
237-
val horizontalSpace = if (length == OudsPinCodeInputLength.Eight) 6.dp else pinCodeInputTokens.spaceColumnGapDigitInput.value
238-
val totalSpace = horizontalSpace * (length.value - 1)
239-
val digitWidth = (maxWidth - totalSpace) / length.value
240-
ConstraintLayout {
241-
val (row, helperTextErrorMessage) = createRefs()
242-
Row(
243-
modifier = Modifier
244-
.constrainAs(row) {
245-
top.linkTo(parent.top)
246-
start.linkTo(parent.start)
247-
end.linkTo(parent.end)
248-
},
249-
horizontalArrangement = Arrangement.spacedBy(horizontalSpace)
250-
) {
251-
repeat(length.value) { index ->
252-
val isNonErrorPreview = LocalInspectionMode.current && error == null
253-
val focusedDigitIndex = (textFieldState.selection.end - 1).coerceIn(0, length.value - 1)
254-
val digitInputState = when {
255-
(isNonErrorPreview || interactionState == InteractionState.Focused) && index == focusedDigitIndex -> OudsDigitInputState.Focused
256-
interactionState == InteractionState.Hovered -> OudsDigitInputState.Hovered
257-
else -> OudsDigitInputState.Enabled
258-
}
259-
OudsDigitInput(
260-
modifier = Modifier.width(digitWidth),
261-
digit = value.getOrNull(index),
262-
onClick = {
263-
focusRequester.requestFocus()
264-
// If keyboard is dismissed using the Android back key, the keyboard won't reappear when digit is clicked
265-
keyboardController?.show()
266-
textFieldState.edit { placeCursorAfterCharAt(index) }
267-
},
268-
state = digitInputState,
269-
outlined = outlined,
270-
error = error != null,
271-
placeholder = error == null,
272-
horizontalPadding = if (length == OudsPinCodeInputLength.Eight) 0.dp else OudsDigitInputDefaults.horizontalPadding
273-
)
274-
}
275-
}
276-
OudsTextInputHelperTextErrorMessage(
277-
modifier = Modifier.constrainAs(helperTextErrorMessage) {
278-
top.linkTo(row.bottom)
279-
bottom.linkTo(parent.bottom)
280-
start.linkTo(row.start)
281-
end.linkTo(row.end)
282-
width = Dimension.fillToConstraints
283-
},
284-
enabled = true,
285-
error = error,
286-
helperText = helperText
287-
)
253+
repeat(length.value) { index ->
254+
val isNonErrorPreview = LocalInspectionMode.current && error == null
255+
val focusedDigitIndex = (textFieldState.selection.end - 1).coerceIn(0, length.value - 1)
256+
val digitInputState = when {
257+
(isNonErrorPreview || interactionState == InteractionState.Focused) && index == focusedDigitIndex -> OudsDigitInputState.Focused
258+
interactionState == InteractionState.Hovered -> OudsDigitInputState.Hovered
259+
else -> OudsDigitInputState.Enabled
288260
}
261+
OudsDigitInput(
262+
modifier = Modifier.width(digitWidth),
263+
digit = textFieldState.text.getOrNull(index),
264+
onClick = {
265+
onDigitClick(index)
266+
textFieldState.edit { placeCursorAfterCharAt(index) }
267+
},
268+
state = digitInputState,
269+
outlined = outlined,
270+
error = error != null,
271+
placeholder = error == null,
272+
horizontalPadding = if (length == OudsPinCodeInputLength.Eight) 0.dp else OudsDigitInputDefaults.horizontalPadding
273+
)
289274
}
290275
}
276+
OudsTextInputHelperTextErrorMessage(
277+
modifier = Modifier.constrainAs(helperTextErrorMessage) {
278+
top.linkTo(row.bottom)
279+
bottom.linkTo(parent.bottom)
280+
start.linkTo(row.start)
281+
end.linkTo(row.end)
282+
width = Dimension.fillToConstraints
283+
},
284+
enabled = true,
285+
error = error,
286+
helperText = helperText
287+
)
291288
}
292-
)
289+
}
290+
}
291+
292+
@OptIn(ExperimentalFoundationApi::class)
293+
private fun inputTransformation(length: OudsPinCodeInputLength): InputTransformation {
294+
return InputTransformation {
295+
changes.forEachChangeReversed { range, originalRange ->
296+
// Insertion
297+
if (range.length > 0) {
298+
val pasting = range.length > 1
299+
val baseText = if (pasting) OudsDigitInputPlaceholder.toString().repeat(length.value) else originalText.toString()
300+
// Retrieve added text
301+
val addedText = asCharSequence().substring(range)
302+
// Roll back to the original text or placeholders if pasting
303+
delete(0, this.length)
304+
insert(0, baseText)
305+
// Replace the base text with the added text
306+
// When pasting (i.e. range.length > 1), the base text is replaced from the start
307+
val start = if (pasting) 0 else range.min - 1
308+
val end = if (pasting) addedText.length else range.max - 1
309+
replace(start.coerceIn(0, length.value), end.coerceIn(0, length.value), addedText)
310+
placeCursorAfterCharAt(end.coerceIn(0, length.value - 1))
311+
}
312+
// Deletion
313+
else {
314+
// Insert placeholder digits to replace the deleted text
315+
val padText = OudsDigitInputPlaceholder.toString().repeat(originalRange.length)
316+
insert(originalRange.start, padText)
317+
// Get the first character of the deleted text
318+
// If that character was not a digit (i.e. a placeholder was displayed in the digit input)
319+
// then replace the previous character with a placeholder
320+
val firstDeletedChar = originalText[originalRange.start]
321+
val previousChar = asCharSequence().getOrNull(range.start - 1)
322+
if (!firstDeletedChar.isDigit() && previousChar != null) {
323+
replace(range.start - 1, range.start, OudsDigitInputPlaceholder.toString())
324+
placeCursorAfterCharAt(range.start - 1)
325+
}
326+
}
327+
}
328+
}
293329
}
294330

295331
/**

0 commit comments

Comments
 (0)