From 76942c5514d84910619ce488998fd6abd5d3f303 Mon Sep 17 00:00:00 2001 From: Kirill Zyusko Date: Mon, 9 Dec 2024 13:42:04 +0100 Subject: [PATCH] feat: type (#722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📜 Description Expose `type` property in `EventEmitter` events. ## 💡 Motivation and Context Added a new property to `KeyboardEventData` - `type`. This property reflects `keyboardType` set on `TextInput`. I decided to include more information about the keyboard (we already have a full control over animation properties, but I thought it could be useful to extend the library functionality and include some other properties, such as `type`/`appearance`/`capitalized` and other). As of now there is no much sense in these properties, but I'm thinking about new API `KeyboardController.retain()`/`KeyboardController.hold()` - and in this case it's super useful to know these properties, to re-create the same `TextInput` and set focus to this newly created input. However it's only plans, but having this information in the API can be useful, so I decided to add it now. ## 📢 Changelog ### Docs - added a reference to a new property; - added description to all events; ### JS - expose `type` from `KeyboardEvents` events; - updated example app; ### iOS - added `UIKeyboardType` extension; - move events creation function to its separate `KeyboardEventEmitterPayload.swift` file to cleanup `KeyboardMovementObserver`; ### Android - added `EditText.keyboardType` extension; ## 🤔 How Has This Been Tested? Tested manually on: - Pixel 3a API 33 (emulator); - iPhone 15 Pro (iOS 17.5). ## 📸 Screenshots (if appropriate): ![image](https://github.com/user-attachments/assets/0e1ba9c0-41ea-4835-8954-12b64b3853ca) ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed --- .../src/screens/Examples/Events/index.tsx | 10 +++--- .../extensions/EditText.kt | 35 +++++++++++++++++++ .../listeners/KeyboardAnimationCallback.kt | 3 ++ docs/docs/api/keyboard-events.md | 9 ++--- example/src/screens/Examples/Events/index.tsx | 10 +++--- ios/events/KeyboardEventEmitterPayload.swift | 22 ++++++++++++ ios/extensions/UIKeyboardType.swift | 30 ++++++++++++++++ ios/observers/KeyboardMovementObserver.swift | 18 +++------- ios/protocols/TextInput.swift | 1 + src/types.ts | 2 ++ 10 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 ios/events/KeyboardEventEmitterPayload.swift create mode 100644 ios/extensions/UIKeyboardType.swift diff --git a/FabricExample/src/screens/Examples/Events/index.tsx b/FabricExample/src/screens/Examples/Events/index.tsx index 00c679fb66..a7d4bbc283 100644 --- a/FabricExample/src/screens/Examples/Events/index.tsx +++ b/FabricExample/src/screens/Examples/Events/index.tsx @@ -24,7 +24,7 @@ function EventsListener() { Toast.show({ type: "info", text1: "⬆️ ⌨️ Keyboard will show", - text2: `📲 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms`, + text2: `📲 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms, type: ${e.type}`, }); }); const shown = KeyboardEvents.addListener("keyboardDidShow", (e) => { @@ -33,7 +33,7 @@ function EventsListener() { Toast.show({ type: "success", text1: "⌨️ Keyboard is shown", - text2: `👋 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms`, + text2: `👋 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms, type: ${e.type}`, }); }); const hide = KeyboardEvents.addListener("keyboardWillHide", (e) => { @@ -42,7 +42,7 @@ function EventsListener() { Toast.show({ type: "info", text1: "⬇️ ⌨️ Keyboard will hide", - text2: `📲 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms`, + text2: `📲 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms, type: ${e.type}`, }); }); const hid = KeyboardEvents.addListener("keyboardDidHide", (e) => { @@ -51,7 +51,7 @@ function EventsListener() { Toast.show({ type: "error", text1: "⌨️ Keyboard is hidden", - text2: `🤐 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms`, + text2: `🤐 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms, type: ${e.type}`, }); }); @@ -63,7 +63,7 @@ function EventsListener() { }; }, []); - return ; + return ; } export default function Events() { diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt index f2dd54bd7e..5045eaec76 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt @@ -2,6 +2,7 @@ package com.reactnativekeyboardcontroller.extensions import android.os.Build import android.text.Editable +import android.text.InputType import android.text.TextWatcher import android.view.View import android.widget.EditText @@ -110,6 +111,40 @@ fun EditText?.focus() { } } +val EditText?.keyboardType: String + get() { + if (this == null) { + return "default" + } + + // Extract base input type class + val inputTypeClass = inputType and InputType.TYPE_MASK_CLASS + val inputTypeVariation = inputType and InputType.TYPE_MASK_VARIATION + + // Check for special input types + return when { + inputTypeVariation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> "email-address" + inputTypeVariation == InputType.TYPE_TEXT_VARIATION_URI -> "url" + inputTypeVariation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> "visible-password" + + // Check for specific input type classes + inputTypeClass == InputType.TYPE_CLASS_NUMBER -> + when { + (inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL) != 0 && + (inputType and InputType.TYPE_NUMBER_FLAG_SIGNED) == 0 -> "decimal-pad" + + (inputType and InputType.TYPE_NUMBER_FLAG_SIGNED) != 0 -> "numeric" + + else -> "number-pad" + } + + inputTypeClass == InputType.TYPE_CLASS_PHONE -> "phone-pad" + inputTypeClass == InputType.TYPE_CLASS_TEXT -> "default" + + else -> "default" + } + } + class KeyboardControllerSelectionWatcher( private val editText: ReactEditText, private val action: (start: Int, end: Int, startX: Double, startY: Double, endX: Double, endY: Double) -> Unit, diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt index 191591025d..2a06f8d6c0 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -19,8 +19,10 @@ import com.reactnativekeyboardcontroller.extensions.dispatchEvent import com.reactnativekeyboardcontroller.extensions.dp import com.reactnativekeyboardcontroller.extensions.emitEvent import com.reactnativekeyboardcontroller.extensions.isKeyboardAnimation +import com.reactnativekeyboardcontroller.extensions.keyboardType import com.reactnativekeyboardcontroller.interactive.InteractiveKeyboardProvider import com.reactnativekeyboardcontroller.log.Logger +import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder import kotlin.math.abs private val TAG = KeyboardAnimationCallback::class.qualifiedName @@ -425,6 +427,7 @@ class KeyboardAnimationCallback( params.putInt("duration", duration) params.putDouble("timestamp", System.currentTimeMillis().toDouble()) params.putInt("target", viewTagFocused) + params.putString("type", FocusedInputHolder.get()?.keyboardType) return params } diff --git a/docs/docs/api/keyboard-events.md b/docs/docs/api/keyboard-events.md index 9920b5f0dd..c6e550c17c 100644 --- a/docs/docs/api/keyboard-events.md +++ b/docs/docs/api/keyboard-events.md @@ -16,10 +16,10 @@ keywords: This library exposes 4 events which are available on all platforms: -- keyboardWillShow -- keyboardWillHide -- keyboardDidShow -- keyboardDidHide +- `keyboardWillShow` - emitted when the keyboard is about to appear. +- `keyboardWillHide` - emitted when the keyboard is about to disappear. +- `keyboardDidShow` - emitted when the keyboard has completed its animation and is fully visible on the screen. +- `keyboardDidHide` - emitted when the keyboard has completed its animation and is fully hidden. ## Event structure @@ -31,6 +31,7 @@ type KeyboardEventData = { duration: number; // duration of the animation timestamp: number; // timestamp of the event from native thread target: number; // tag of the focused TextInput + type: string; // `keyboardType` property from focused `TextInput` }; ``` diff --git a/example/src/screens/Examples/Events/index.tsx b/example/src/screens/Examples/Events/index.tsx index 00c679fb66..a7d4bbc283 100644 --- a/example/src/screens/Examples/Events/index.tsx +++ b/example/src/screens/Examples/Events/index.tsx @@ -24,7 +24,7 @@ function EventsListener() { Toast.show({ type: "info", text1: "⬆️ ⌨️ Keyboard will show", - text2: `📲 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms`, + text2: `📲 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms, type: ${e.type}`, }); }); const shown = KeyboardEvents.addListener("keyboardDidShow", (e) => { @@ -33,7 +33,7 @@ function EventsListener() { Toast.show({ type: "success", text1: "⌨️ Keyboard is shown", - text2: `👋 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms`, + text2: `👋 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms, type: ${e.type}`, }); }); const hide = KeyboardEvents.addListener("keyboardWillHide", (e) => { @@ -42,7 +42,7 @@ function EventsListener() { Toast.show({ type: "info", text1: "⬇️ ⌨️ Keyboard will hide", - text2: `📲 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms`, + text2: `📲 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms, type: ${e.type}`, }); }); const hid = KeyboardEvents.addListener("keyboardDidHide", (e) => { @@ -51,7 +51,7 @@ function EventsListener() { Toast.show({ type: "error", text1: "⌨️ Keyboard is hidden", - text2: `🤐 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms`, + text2: `🤐 Height: ${e.height}, duration: ${e.duration}ms, delay: ${delay}ms, type: ${e.type}`, }); }); @@ -63,7 +63,7 @@ function EventsListener() { }; }, []); - return ; + return ; } export default function Events() { diff --git a/ios/events/KeyboardEventEmitterPayload.swift b/ios/events/KeyboardEventEmitterPayload.swift new file mode 100644 index 0000000000..f4777d6bea --- /dev/null +++ b/ios/events/KeyboardEventEmitterPayload.swift @@ -0,0 +1,22 @@ +// +// KeyboardEventEmitterPayload.swift +// Pods +// +// Created by Kiryl Ziusko on 07/12/2024. +// + +import Foundation +import UIKit + +public func buildEventParams(_ height: Double, _ duration: Int, _ tag: NSNumber) -> [AnyHashable: Any] { + var data = [AnyHashable: Any]() + let input = FocusedInputHolder.shared.get() + + data["height"] = height + data["duration"] = duration + data["timestamp"] = Date.currentTimeStamp + data["target"] = tag + data["type"] = input?.keyboardType.name ?? "default" + + return data +} diff --git a/ios/extensions/UIKeyboardType.swift b/ios/extensions/UIKeyboardType.swift new file mode 100644 index 0000000000..65715f6147 --- /dev/null +++ b/ios/extensions/UIKeyboardType.swift @@ -0,0 +1,30 @@ +// +// UIKeyboardType.swift +// Pods +// +// Created by Kiryl Ziusko on 08/12/2024. +// + +import Foundation +import UIKit + +extension UIKeyboardType { + private static let keyboardTypeToStringMapping: [UIKeyboardType: String] = [ + .default: "default", + .asciiCapable: "ascii-capable", + .numbersAndPunctuation: "numbers-and-punctuation", + .URL: "url", + .numberPad: "number-pad", + .phonePad: "phone-pad", + .namePhonePad: "name-phone-pad", + .emailAddress: "email-address", + .decimalPad: "decimal-pad", + .twitter: "twitter", + .webSearch: "web-search", + .asciiCapableNumberPad: "ascii-capable-number-pad", + ] + + var name: String { + return UIKeyboardType.keyboardTypeToStringMapping[self] ?? "default" + } +} diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index 68871b15a5..d281499d09 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -173,7 +173,7 @@ public class KeyboardMovementObserver: NSObject { onRequestAnimation() onEvent("onKeyboardMoveStart", Float(keyboardHeight) as NSNumber, 1, duration as NSNumber, tag) - onNotify("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight, duration)) + onNotify("KeyboardController::keyboardWillShow", buildEventParams(keyboardHeight, duration, tag)) setupKeyboardWatcher() initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight) @@ -187,7 +187,7 @@ public class KeyboardMovementObserver: NSObject { onRequestAnimation() onEvent("onKeyboardMoveStart", 0, 0, duration as NSNumber, tag) - onNotify("KeyboardController::keyboardWillHide", getEventParams(0, duration)) + onNotify("KeyboardController::keyboardWillHide", buildEventParams(0, duration, tag)) setupKeyboardWatcher() removeKVObserver() @@ -210,7 +210,7 @@ public class KeyboardMovementObserver: NSObject { onCancelAnimation() onEvent("onKeyboardMoveEnd", height as NSNumber, progress as NSNumber, duration as NSNumber, tag) - onNotify("KeyboardController::keyboardDidShow", getEventParams(height, duration)) + onNotify("KeyboardController::keyboardDidShow", buildEventParams(height, duration, tag)) removeKeyboardWatcher() setupKVObserver() @@ -224,7 +224,7 @@ public class KeyboardMovementObserver: NSObject { onCancelAnimation() onEvent("onKeyboardMoveEnd", 0 as NSNumber, 0, duration as NSNumber, tag) - onNotify("KeyboardController::keyboardDidHide", getEventParams(0, duration)) + onNotify("KeyboardController::keyboardDidHide", buildEventParams(0, duration, tag)) removeKeyboardWatcher() animation = nil @@ -309,14 +309,4 @@ public class KeyboardMovementObserver: NSObject { tag ) } - - private func getEventParams(_ height: Double, _ duration: Int) -> [AnyHashable: Any] { - var data = [AnyHashable: Any]() - data["height"] = height - data["duration"] = duration - data["timestamp"] = Date.currentTimeStamp - data["target"] = tag - - return data - } } diff --git a/ios/protocols/TextInput.swift b/ios/protocols/TextInput.swift index deb1458453..f24c9ca6ae 100644 --- a/ios/protocols/TextInput.swift +++ b/ios/protocols/TextInput.swift @@ -10,6 +10,7 @@ import Foundation import UIKit public protocol TextInput: AnyObject { + var keyboardType: UIKeyboardType { get } func focus() } diff --git a/src/types.ts b/src/types.ts index b8149eb6eb..a57437a19d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import type { PropsWithChildren } from "react"; import type { EmitterSubscription, NativeSyntheticEvent, + TextInputProps, ViewProps, } from "react-native"; @@ -147,6 +148,7 @@ export type KeyboardEventData = { duration: number; timestamp: number; target: number; + type: TextInputProps["keyboardType"]; }; export type KeyboardEventsModule = { addListener: (