From 0b1f53c4334ace0bfa86c56e1d9737369a93b727 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 9 Dec 2023 21:18:49 +0100 Subject: [PATCH] feat: new `useFocusedInputHandler` hook --- .../__tests__/focused-input-handler.spec.tsx | 50 +++++++++++++++++++ .../KeyboardAwareScrollView.tsx | 8 ++- .../hooks/input/use-focused-input-handler.md | 34 +++++++++++++ .../input/use-reanimated-focused-input.md | 12 ++--- .../input/use-reanimated-focused-input.md | 2 +- .../__tests__/focused-input-handler.spec.tsx | 50 +++++++++++++++++++ .../KeyboardAwareScrollView.tsx | 8 ++- jest/index.js | 1 + src/hooks.ts | 10 ++-- 9 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 FabricExample/__tests__/focused-input-handler.spec.tsx create mode 100644 docs/docs/api/hooks/input/use-focused-input-handler.md create mode 100644 example/__tests__/focused-input-handler.spec.tsx diff --git a/FabricExample/__tests__/focused-input-handler.spec.tsx b/FabricExample/__tests__/focused-input-handler.spec.tsx new file mode 100644 index 0000000000..9619c48ce4 --- /dev/null +++ b/FabricExample/__tests__/focused-input-handler.spec.tsx @@ -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}; +} + +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(); + + 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(''); + }); +}); diff --git a/FabricExample/src/components/AwareScrollView/KeyboardAwareScrollView.tsx b/FabricExample/src/components/AwareScrollView/KeyboardAwareScrollView.tsx index 584c113d67..e19d60978c 100644 --- a/FabricExample/src/components/AwareScrollView/KeyboardAwareScrollView.tsx +++ b/FabricExample/src/components/AwareScrollView/KeyboardAwareScrollView.tsx @@ -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, @@ -105,7 +109,7 @@ const KeyboardAwareScrollView: FC = ({ return 0; }, [bottomOffset]); - useReanimatedFocusedInput({ + useFocusedInputHandler({ onChangeText: ({text}) => { 'worklet'; diff --git a/docs/docs/api/hooks/input/use-focused-input-handler.md b/docs/docs/api/hooks/input/use-focused-input-handler.md new file mode 100644 index 0000000000..220a1b657a --- /dev/null +++ b/docs/docs/api/hooks/input/use-focused-input-handler.md @@ -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. diff --git a/docs/docs/api/hooks/input/use-reanimated-focused-input.md b/docs/docs/api/hooks/input/use-reanimated-focused-input.md index deb774ff8d..e27c6258ac 100644 --- a/docs/docs/api/hooks/input/use-reanimated-focused-input.md +++ b/docs/docs/api/hooks/input/use-reanimated-focused-input.md @@ -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. @@ -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 @@ -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. diff --git a/docs/versioned_docs/version-1.9.0/api/hooks/input/use-reanimated-focused-input.md b/docs/versioned_docs/version-1.9.0/api/hooks/input/use-reanimated-focused-input.md index 12e6133f20..d0f2a96388 100644 --- a/docs/versioned_docs/version-1.9.0/api/hooks/input/use-reanimated-focused-input.md +++ b/docs/versioned_docs/version-1.9.0/api/hooks/input/use-reanimated-focused-input.md @@ -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 diff --git a/example/__tests__/focused-input-handler.spec.tsx b/example/__tests__/focused-input-handler.spec.tsx new file mode 100644 index 0000000000..9619c48ce4 --- /dev/null +++ b/example/__tests__/focused-input-handler.spec.tsx @@ -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}; +} + +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(); + + 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(''); + }); +}); diff --git a/example/src/components/AwareScrollView/KeyboardAwareScrollView.tsx b/example/src/components/AwareScrollView/KeyboardAwareScrollView.tsx index 14b97881b8..7aa5ac322e 100644 --- a/example/src/components/AwareScrollView/KeyboardAwareScrollView.tsx +++ b/example/src/components/AwareScrollView/KeyboardAwareScrollView.tsx @@ -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, @@ -105,7 +109,7 @@ const KeyboardAwareScrollView: FC = ({ return 0; }, [bottomOffset]); - useReanimatedFocusedInput({ + useFocusedInputHandler({ onChangeText: ({text}) => { 'worklet'; diff --git a/jest/index.js b/jest/index.js index 4b23331044..1f01c759d0 100644 --- a/jest/index.js +++ b/jest/index.js @@ -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(), diff --git a/src/hooks.ts b/src/hooks.ts index 05d059639b..f2db29e5d8 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -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 ) { @@ -79,6 +85,4 @@ export function useReanimatedFocusedInput( context.setInputHandlers({ [key]: undefined }); }; }, deps); - - return { input: context.layout }; }