Skip to content

Commit

Permalink
feat: new useFocusedInputHandler hook
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillzyusko committed Dec 9, 2023
1 parent 85c3293 commit 0b1f53c
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 17 deletions.
50 changes: 50 additions & 0 deletions FabricExample/__tests__/focused-input-handler.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import '@testing-library/jest-native/extend-expect';
import React, { useState } from 'react';
import { runOnJS } from 'react-native-reanimated';
import { act, render } from '@testing-library/react-native';

import { FocusedInputHandler, FocusedInputTextChangedEvent, useFocusedInputHandler, useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import { Text } from 'react-native';

function WhatUserTyped() {
const [text, setText] = useState('');

useFocusedInputHandler({
onChangeText: (e) => {
'worklet';

runOnJS(setText)(e.text);
},
});

return <Text testID='text'>{text}</Text>;
}

describe('`useFocusedInputHandler` specification', () => {
it('should execute all handlers and change corresponding elements', () => {
let handlers: FocusedInputHandler = {};
(useFocusedInputHandler as jest.Mock).mockImplementation(
(handler) => (handlers = handler)
);
const onChangeText = (e: FocusedInputTextChangedEvent) => handlers.onChangeText?.(e);

const { getByTestId } = render(<WhatUserTyped />);

expect(getByTestId('text')).toHaveTextContent('');
act(() => onChangeText({text: '1'}));

expect(getByTestId('text')).toHaveTextContent('1');

act(() => onChangeText({text: '12'}));

expect(getByTestId('text')).toHaveTextContent('12');

act(() => onChangeText({text: '123'}));

expect(getByTestId('text')).toHaveTextContent('123');

act(() => onChangeText({text: ''}));

expect(getByTestId('text')).toHaveTextContent('');
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { FC, useCallback } from 'react';
import { ScrollViewProps, useWindowDimensions } from 'react-native';
import { FocusedInputLayoutChangedEvent, useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import {
FocusedInputLayoutChangedEvent,
useFocusedInputHandler,
useReanimatedFocusedInput
} from 'react-native-keyboard-controller';
import Reanimated, {
interpolate,
scrollTo,
Expand Down Expand Up @@ -105,7 +109,7 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
return 0;
}, [bottomOffset]);

useReanimatedFocusedInput({
useFocusedInputHandler({
onChangeText: ({text}) => {
'worklet';

Expand Down
34 changes: 34 additions & 0 deletions docs/docs/api/hooks/input/use-focused-input-handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
keywords: [react-native, react native, react-native-keyboard-controller, useFocusedInputHandler, onTextChanged, onChangeText, input interceptor, react-native-reanimated, worklet, react hook]
---

# useFocusedInputHandler

`useFocusedInputHandler` is a hook that allows to intercept events from a focused `TextInput`.

## Example

```ts
useFocusedInputHandler({
onChangeText: ({text}) => {
'worklet';
}
}, []);
```

### Handlers

#### `onChangeText`

Fires an event whenever user changes text in focused `TextInput` (i. e. adds or deletes symbols). Event has following structure:

```ts
type FocusedInputTextChangedEvent = {
text: string;
};
```

This handler can be handy when you need to have an access to what user typed on a global level (i. e. when you don't have a direct access to your `TextInput`), for example:

- you develop a generic component for any kind of avoidance focused inputs (i. e. `AwareScrollView`) that doesn't have an access to child `TextInputs` by design;
- you track user activity on the screen and if there is no activity for certain period of time then you do a certain action (logout for example). If you want to reset timer when user interacts with a keyboard - usage of this hook can be a good choice.
12 changes: 3 additions & 9 deletions docs/docs/api/hooks/input/use-reanimated-focused-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Hook will update its value in next cases:
- when keyboard changes its size (appears, disappears, changes size because of different input mode);
- when focus was changed from one `TextInput` to another;
- when `layout` of focused input was changed;
- when user types a text;
- when user types a text.

:::info Events order
The value from `useReanimatedFocusedInput` will be always updated before keyboard events, so you can safely read values in `onStart` handler and be sure they are up-to-date.
Expand All @@ -22,7 +22,7 @@ The value from `useReanimatedFocusedInput` will be always updated before keyboar
The `input` property from this hook is returned as `SharedValue`. The returned data has next structure:

```ts
type KeyboardEventData = {
type FocusedInputLayoutChangedEvent = {
target: number; // tag of the focused TextInput
layout: { // layout of the focused TextInput
x: number; // `x` coordinate inside the parent component
Expand All @@ -38,13 +38,7 @@ type KeyboardEventData = {
## Example

```tsx
const {input} = useReanimatedFocusedInput({
onChangeText: ({text}) => {
'worklet';

// ...
}
}, []);
const {input} = useReanimatedFocusedInput();
```

Also have a look on [example](https://github.com/kirillzyusko/react-native-keyboard-controller/tree/main/example) app for more comprehensive usage.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The value from `useReanimatedFocusedInput` will be always updated before keyboar
The `input` property from this hook is returned as `SharedValue`. The returned data has next structure:

```ts
type KeyboardEventData = {
type FocusedInputLayoutChangedEvent = {
target: number; // tag of the focused TextInput
layout: { // layout of the focused TextInput
x: number; // `x` coordinate inside the parent component
Expand Down
50 changes: 50 additions & 0 deletions example/__tests__/focused-input-handler.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import '@testing-library/jest-native/extend-expect';
import React, { useState } from 'react';
import { runOnJS } from 'react-native-reanimated';
import { act, render } from '@testing-library/react-native';

import { FocusedInputHandler, FocusedInputTextChangedEvent, useFocusedInputHandler, useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import { Text } from 'react-native';

function WhatUserTyped() {
const [text, setText] = useState('');

useFocusedInputHandler({
onChangeText: (e) => {
'worklet';

runOnJS(setText)(e.text);
},
});

return <Text testID='text'>{text}</Text>;
}

describe('`useFocusedInputHandler` specification', () => {
it('should execute all handlers and change corresponding elements', () => {
let handlers: FocusedInputHandler = {};
(useFocusedInputHandler as jest.Mock).mockImplementation(
(handler) => (handlers = handler)
);
const onChangeText = (e: FocusedInputTextChangedEvent) => handlers.onChangeText?.(e);

const { getByTestId } = render(<WhatUserTyped />);

expect(getByTestId('text')).toHaveTextContent('');
act(() => onChangeText({text: '1'}));

expect(getByTestId('text')).toHaveTextContent('1');

act(() => onChangeText({text: '12'}));

expect(getByTestId('text')).toHaveTextContent('12');

act(() => onChangeText({text: '123'}));

expect(getByTestId('text')).toHaveTextContent('123');

act(() => onChangeText({text: ''}));

expect(getByTestId('text')).toHaveTextContent('');
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { FC, useCallback } from 'react';
import { ScrollViewProps, useWindowDimensions } from 'react-native';
import { FocusedInputLayoutChangedEvent, useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import {
FocusedInputLayoutChangedEvent,
useFocusedInputHandler,
useReanimatedFocusedInput
} from 'react-native-keyboard-controller';
import Reanimated, {
interpolate,
scrollTo,
Expand Down Expand Up @@ -105,7 +109,7 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
return 0;
}, [bottomOffset]);

useReanimatedFocusedInput({
useFocusedInputHandler({
onChangeText: ({text}) => {
'worklet';

Expand Down
1 change: 1 addition & 0 deletions jest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const mock = {
useGenericKeyboardHandler: jest.fn(),
useKeyboardHandler: jest.fn(),
useReanimatedFocusedInput: jest.fn().mockReturnValue(focusedInput),
useFocusedInputHandler: jest.fn(),
// modules
KeyboardController: {
setInputMode: jest.fn(),
Expand Down
10 changes: 7 additions & 3 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ export function useKeyboardController() {
return { setEnabled: context.setEnabled, enabled: context.enabled };
}

export function useReanimatedFocusedInput(
export function useReanimatedFocusedInput() {
const context = useKeyboardContext();

return { input: context.layout };
}

export function useFocusedInputHandler(
handler?: FocusedInputHandler,
deps?: DependencyList
) {
Expand All @@ -79,6 +85,4 @@ export function useReanimatedFocusedInput(
context.setInputHandlers({ [key]: undefined });
};
}, deps);

return { input: context.layout };
}

0 comments on commit 0b1f53c

Please sign in to comment.