@@ -26,11 +26,11 @@ import androidx.compose.foundation.text.BasicSecureTextField
2626import androidx.compose.foundation.text.KeyboardOptions
2727import androidx.compose.foundation.text.input.InputTransformation
2828import androidx.compose.foundation.text.input.KeyboardActionHandler
29+ import androidx.compose.foundation.text.input.TextFieldState
2930import androidx.compose.foundation.text.input.delete
3031import androidx.compose.foundation.text.input.forEachChangeReversed
3132import androidx.compose.foundation.text.input.insert
3233import androidx.compose.foundation.text.input.rememberTextFieldState
33- import androidx.compose.foundation.text.input.then
3434import androidx.compose.material3.ExperimentalMaterial3Api
3535import androidx.compose.material3.LocalRippleConfiguration
3636import 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