Skip to content

Commit

Permalink
feat: type (#722)
Browse files Browse the repository at this point in the history
## 📜 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
  • Loading branch information
kirillzyusko authored Dec 9, 2024
1 parent a3bec0d commit 76942c5
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 28 deletions.
10 changes: 5 additions & 5 deletions FabricExample/src/screens/Examples/Events/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand All @@ -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) => {
Expand All @@ -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}`,
});
});

Expand All @@ -63,7 +63,7 @@ function EventsListener() {
};
}, []);

return <TextInput style={styles.input} />;
return <TextInput keyboardType="numeric" style={styles.input} />;
}

export default function Events() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 5 additions & 4 deletions docs/docs/api/keyboard-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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`
};
```

Expand Down
10 changes: 5 additions & 5 deletions example/src/screens/Examples/Events/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand All @@ -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) => {
Expand All @@ -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}`,
});
});

Expand All @@ -63,7 +63,7 @@ function EventsListener() {
};
}, []);

return <TextInput style={styles.input} />;
return <TextInput keyboardType="numeric" style={styles.input} />;
}

export default function Events() {
Expand Down
22 changes: 22 additions & 0 deletions ios/events/KeyboardEventEmitterPayload.swift
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions ios/extensions/UIKeyboardType.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}
18 changes: 4 additions & 14 deletions ios/observers/KeyboardMovementObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
1 change: 1 addition & 0 deletions ios/protocols/TextInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
import UIKit

public protocol TextInput: AnyObject {
var keyboardType: UIKeyboardType { get }
func focus()
}

Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PropsWithChildren } from "react";
import type {
EmitterSubscription,
NativeSyntheticEvent,
TextInputProps,
ViewProps,
} from "react-native";

Expand Down Expand Up @@ -147,6 +148,7 @@ export type KeyboardEventData = {
duration: number;
timestamp: number;
target: number;
type: TextInputProps["keyboardType"];
};
export type KeyboardEventsModule = {
addListener: (
Expand Down

0 comments on commit 76942c5

Please sign in to comment.