Pantrix UI

A number field and its action key

Why a field in Compose Multiplatform that a number is typed into, and that does something once it is typed, cannot be sent on an iPhone, what the library gives for it, and how it is tested. The problem is not Pantrix's own: it shows the same way in every Compose Multiplatform project that asks for an ImeAction with KeyboardType.Number in its common code.

Which of what is said here was seen in a simulator, which was read in the sources of Compose Multiplatform 1.12.1, and which was not verified, is written out in section 7.

1. The problem

Symptom

Cause

The iPhone's number pad (UIKeyboardType.numberPad) is designed for entering a PIN and carries no return key. Compose gives a field that asks for KeyboardType.Number this pad on iOS. ImeAction is, in UIKit, the type of the return key (returnKeyType); on a keyboard with no return key it has nowhere to show.

Native iOS apps solve this in one of two ways: they put a bar with a "Done" button above the keyboard (inputAccessoryView), or they ask for a keyboard that has a return key. Compose does neither by itself.

2. The solution

On iOS, a number that has an action asks for the numbers and punctuation keyboard (UIKeyboardType.numbersAndPunctuation): it opens on its digits and has a return key that shows the action.

Compose gives a way for it: KeyboardOptions.platformImeOptions. On iOS, PlatformImeOptions { keyboardType(…) } says the keyboard's type in UIKit's own type, and overrides the one derived from KeyboardType.

The API carries @ExperimentalComposeUiApi and has no stable counterpart: none of Compose's KeyboardTypes is turned into this keyboard on iOS (section 7), and every setting of PlatformImeOptions's builder carries the same mark. The library leans on it in one file alone, in iosMain; no public signature holds an experimental type. If its signature changes in an upgrade of Compose, that file does not compile; if its behaviour changes, NumberKeyboardOnIosTest fails. The keyboard itself is seen only in a simulator: after an upgrade, the field is opened once and looked at.

Three details:

DetailWhat is done
1That keyboard is for writing text: above its keys it shows a bar that offers wordsautoCorrectEnabled = false: the bar goes
2The same keyboard has signs and, one press away, letters tooThe field itself turns down what is not a number: InputTransformation
3A number without an action (ImeAction.Default, ImeAction.None) lacks nothing on the number padIt is left alone: on iOS too it gets the number pad

3. With Pantrix

The library gives one function (com.pantrixui.foundation.platform):

public expect fun numberKeyboardOptions(
    imeAction: ImeAction = ImeAction.Default,
    decimal: Boolean = false
): KeyboardOptions

On Android, on the desktop and on the web it is the type and the action asked for, and no more. On iOS, for a number that has an action, it asks for the numbers and punctuation keyboard, with correction off. decimal = true asks for KeyboardType.Decimal in place of KeyboardType.Number.

val quantity = rememberTextFieldState()
PantrixTextField(
    state = quantity,
    label = "Quantity",
    keyboardOptions = numberKeyboardOptions(ImeAction.Done),
    onKeyboardAction = { onQuantity(quantity.text.toString().toIntOrNull()) },
    inputTransformation = DigitsOnly
)

/** Turns down, whole, a change that holds anything but a digit. */
private val DigitsOnly = InputTransformation {
    if (!asCharSequence().all { it.isDigit() }) revertAllChanges()
}

PantrixPageJump sets up its field with it. The keyboardOptions parameter of PantrixTextField points here too.

4. Without Pantrix

An expect function, and a counterpart for each platform.

4.1. Common code (commonMain)

expect fun numberKeyboardOptions(
    imeAction: ImeAction = ImeAction.Default,
    decimal: Boolean = false
): KeyboardOptions

internal fun plainNumberKeyboardOptions(
    imeAction: ImeAction,
    decimal: Boolean
): KeyboardOptions = KeyboardOptions(
    keyboardType = if (decimal) KeyboardType.Decimal else KeyboardType.Number,
    imeAction = imeAction
)

4.2. iOS (iosMain)

@OptIn(ExperimentalComposeUiApi::class)
actual fun numberKeyboardOptions(
    imeAction: ImeAction,
    decimal: Boolean
): KeyboardOptions {
    val plain = plainNumberKeyboardOptions(imeAction, decimal)
    val needsAKey = imeAction != ImeAction.Default && imeAction != ImeAction.None
    return if (!needsAKey) {
        plain
    } else {
        plain.copy(
            autoCorrectEnabled = false,
            platformImeOptions = PlatformImeOptions { keyboardType(UIKeyboardTypeNumbersAndPunctuation) }
        )
    }
}

PlatformImeOptions and keyboardType: androidx.compose.ui.text.input; UIKeyboardTypeNumbersAndPunctuation: platform.UIKit.

4.3. The other platforms (androidMain, jvmMain, web)

actual fun numberKeyboardOptions(
    imeAction: ImeAction,
    decimal: Boolean
): KeyboardOptions = plainNumberKeyboardOptions(imeAction, decimal)

5. What the helper does not do

6. Testing

A test cannot see the keyboard; what it can see is what the field asks of the keyboard.

That the keyboard really opens with the key, and that the key does the job, is seen only on a device or in a simulator. Every new field that a number is typed into and that has an action should be opened in a simulator.

7. Verification

Seen

Read in the sources

Not verified

8. Where it is in this repository

WhatWhere
The function and its common helperui/foundation/src/commonMain/kotlin/com/pantrixui/foundation/platform/NumberKeyboard.kt
The iOS counterpartui/foundation/src/iosMain/kotlin/com/pantrixui/foundation/platform/NumberKeyboard.ios.kt
The other counterpartsNumberKeyboard.android.kt, NumberKeyboard.jvm.kt, NumberKeyboard.web.kt in the same package
TestsNumberKeyboardTest (commonTest), NumberKeyboardOnIosTest (iosTest)
The component that uses itPantrixPageJump (ui/components/.../data/pagination/PageJump.kt)
The demo"A field of numbers and the action key" on the TextField page; the table's footer on the Pagination page