diff --git a/FabricExample/src/screens/Examples/AwareScrollView/styles.ts b/FabricExample/src/screens/Examples/AwareScrollView/styles.ts index 404787843d..5d9febb505 100644 --- a/FabricExample/src/screens/Examples/AwareScrollView/styles.ts +++ b/FabricExample/src/screens/Examples/AwareScrollView/styles.ts @@ -38,4 +38,7 @@ export const styles = StyleSheet.create({ borderRadius: 8, paddingHorizontal: 12, }, + bottomSheetContent: { + flex: 1, + }, }); diff --git a/FabricExample/src/screens/Examples/Toolbar/index.tsx b/FabricExample/src/screens/Examples/Toolbar/index.tsx index a8d9d2b324..c4b719da0b 100644 --- a/FabricExample/src/screens/Examples/Toolbar/index.tsx +++ b/FabricExample/src/screens/Examples/Toolbar/index.tsx @@ -156,18 +156,26 @@ function Form() { /> - ) : null - } insets={insets} opacity={Platform.OS === "ios" ? "4F" : "DD"} - onDoneCallback={haptic} - onNextCallback={haptic} - onPrevCallback={haptic} - /> + > + + + + + {showAutoFill ? ( + + ) : null} + + + + + ); } @@ -238,12 +246,3 @@ const styles = StyleSheet.create({ marginTop: 32, }, }); - -const blur = ( - -); diff --git a/docs/docs/api/components/keyboard-toolbar/index.mdx b/docs/docs/api/components/keyboard-toolbar/index.mdx index d0fbaee09a..bf0d023949 100644 --- a/docs/docs/api/components/keyboard-toolbar/index.mdx +++ b/docs/docs/api/components/keyboard-toolbar/index.mdx @@ -37,20 +37,60 @@ import toolbar from "./toolbar.lottie.json"; - **Extended accessibility support** 🔍: Ensures that all users, including those with disabilities, can navigate through inputs effectively. - **Full control over the buttons behavior** 🔧: Customize the actions triggered by the next, previous, and done buttons according to your needs. - **Extends ViewProps** 📜: Supports all the props that `View` component has. +- **Compound component pattern** 🔌: Mix and match sub-components for granular control over the toolbar's structure. -## Props +## Compound Components -### [`View Props`](https://reactnative.dev/docs/view#props) +The new API uses sub-components as children of `KeyboardToolbar`. These allow for precise customization, such as conditional rendering of buttons or injecting custom elements. -Inherits [View Props](https://reactnative.dev/docs/view#props). +### `` -### [`KeyboardStickyViewProps`](./keyboard-sticky-view) +Renders a custom background (e.g., blur effect) that overlays the entire toolbar. Accepts any React node as children. -Inherits [KeyboardStickyViewProps](./keyboard-sticky-view). +```tsx +import { Platform } from "react-native"; +import { KeyboardToolbar } from "react-native-keyboard-controller"; +import { BlurView } from "@react-native-community/blur"; + + + + + + {/* Other sub-components */} +; +``` + +:::warning +Please, note, that you need to specify `opacity` prop for this prop to work. Because otherwise you will not see a blur effect. +::: + +### `` + +Renders a custom content (e.g., yours UI elements) in the middle of the toolbar. Accepts any React node as children. + +```tsx +import { KeyboardToolbar } from "react-native-keyboard-controller"; + + + + {showAutoFill ? ( + + ) : null} + + {/* Other sub-components */} +; +``` + +### `` -### `button` +#### `button` -This property allows to render custom touchable component for next, previous and done button. +This property allows to render custom touchable component. ```tsx import { TouchableOpacity } from "react-native-gesture-handler"; @@ -66,101 +106,122 @@ const CustomButton: KeyboardToolbarProps["button"] = ({ // ... -; + + +; ``` -### `blur` - -This property allows to render custom blur effect for the toolbar (by default iOS keyboard is opaque and it blurs the content underneath, so if you want to follow **HIG** ([_Human Interface Guidelines_](https://developer.apple.com/design/human-interface-guidelines/materials)) properly - consider to add this effect). +#### `icon` -By default it is `null` and will not render any blur effect, because it's not a responsibility of this library to provide a blur effect. Instead it provides a property where you can specify your own blur effect and its provider, i. e. `@react-native-community/blur`, `expo-blur` or maybe even `react-native-skia` (based on your project preferences of course). - -:::warning -Please, note, that you need to specify `opacity` prop for this prop to work. Because otherwise you will not see a blur effect. -::: +`icon` property allows to render custom icons. ```tsx -import { BlurView } from "@react-native-community/blur"; +import { Text } from "react-native"; import { KeyboardToolbar, KeyboardToolbarProps, } from "react-native-keyboard-controller"; -const CustomBlur: KeyboardToolbarProps["blur"] = ({ children }) => ( - - {children} - -); +const Icon: KeyboardToolbarProps["icon"] = ({ type }) => { + return {type === "next" ? "⬇️" : "⬆️"}; +}; // ... -; + + +; ``` -### `content` +#### `onPress` -This property allows you to show a custom content in the middle of the toolbar. It accepts JSX element. Default value is `null`. +A callback that is called when the user presses the **previous** button. The callback receives an instance of `GestureResponderEvent` which can be used to cancel the default action (for advanced use-cases). ```tsx - - ) : null - } -/> -``` +import { Platform } from "react-native"; +import { KeyboardToolbar } from "react-native-keyboard-controller"; +import { trigger } from "react-native-haptic-feedback"; + +const options = { + enableVibrateFallback: true, + ignoreAndroidSystemSettings: false, +}; +const haptic = () => + trigger(Platform.OS === "ios" ? "impactLight" : "keyboardTap", options); -### `doneText` +// ... -The property that allows to specify custom text for `Done` button. + + +; +``` + +:::tip Prevent Default Action +To prevent the default action, call `e.preventDefault()` inside the callback: ```tsx - + + { + // the focus will not be moved to the prev input + e.preventDefault(); + }} + /> + ``` -### `icon` +::: + +### `` -`icon` property allows to render custom icons for prev and next buttons. +#### `button` + +This property allows to render custom touchable component. ```tsx -import { Text } from "react-native"; +import { TouchableOpacity } from "react-native-gesture-handler"; import { KeyboardToolbar, KeyboardToolbarProps, } from "react-native-keyboard-controller"; -const Icon: KeyboardToolbarProps["icon"] = ({ type }) => { - return {type === "next" ? "⬇️" : "⬆️"}; -}; +const CustomButton: KeyboardToolbarProps["button"] = ({ + children, + onPress, +}) => {children}; // ... -; + + +; ``` -### `insets` +#### `icon` -An object containing `left` and `right` properties that define the `KeyboardToolbar` padding. This helps prevent overlap with system UI elements, especially in landscape orientation: +`icon` property allows to render custom icons. ```tsx -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Text } from "react-native"; +import { + KeyboardToolbar, + KeyboardToolbarProps, +} from "react-native-keyboard-controller"; -// ... +const Icon: KeyboardToolbarProps["icon"] = ({ type }) => { + return {type === "next" ? "⬇️" : "⬆️"}; +}; -const insets = useSafeAreaInsets(); +// ... -; + + +; ``` -### `onDoneCallback` +#### `onPress` -A callback that is called when the user presses the **done** button. The callback receives an instance of `GestureResponderEvent` which can be used to cancel the default action (for advanced use-cases). +A callback that is called when the user presses the **next** button. The callback receives an instance of `GestureResponderEvent` which can be used to cancel the default action (for advanced use-cases). ```tsx import { Platform } from "react-native"; @@ -176,25 +237,55 @@ const haptic = () => // ... -; + + +; ``` :::tip Prevent Default Action To prevent the default action, call `e.preventDefault()` inside the callback: ```tsx - { - e.preventDefault(); // keyboard will not be dismissed, since we cancelled the default action - }} -/> + + { + // the focus will not be moved to the next input + e.preventDefault(); + }} + /> + ``` ::: -### `onNextCallback` +### `` -A callback that is called when the user presses the **next** button. The callback receives an instance of `GestureResponderEvent` which can be used to cancel the default action (for advanced use-cases). +#### `button` + +This property allows to render custom touchable component. + +```tsx +import { TouchableOpacity } from "react-native-gesture-handler"; +import { + KeyboardToolbar, + KeyboardToolbarProps, +} from "react-native-keyboard-controller"; + +const CustomButton: KeyboardToolbarProps["button"] = ({ + children, + onPress, +}) => {children}; + +// ... + + + +; +``` + +#### `onPress` + +A callback that is called when the user presses the **done** button. The callback receives an instance of `GestureResponderEvent` which can be used to cancel the default action (for advanced use-cases). ```tsx import { Platform } from "react-native"; @@ -210,55 +301,60 @@ const haptic = () => // ... -; + + +; ``` :::tip Prevent Default Action To prevent the default action, call `e.preventDefault()` inside the callback: ```tsx - { - e.preventDefault(); // the focus will not be moved to the next input - }} -/> + + { + // keyboard will not be dismissed, since we cancelled the default action + e.preventDefault(); + }} + /> + ``` ::: -### `onPrevCallback` +#### `text` -A callback that is called when the user presses the **previous** button. The callback receives an instance of `GestureResponderEvent` which can be used to cancel the default action (for advanced use-cases). +The property that allows to specify custom text for `Done` button. ```tsx -import { Platform } from "react-native"; -import { KeyboardToolbar } from "react-native-keyboard-controller"; -import { trigger } from "react-native-haptic-feedback"; + + + +``` -const options = { - enableVibrateFallback: true, - ignoreAndroidSystemSettings: false, -}; -const haptic = () => - trigger(Platform.OS === "ios" ? "impactLight" : "keyboardTap", options); +## Props -// ... +### [`View Props`](https://reactnative.dev/docs/view#props) -; -``` +Inherits [View Props](https://reactnative.dev/docs/view#props). -:::tip Prevent Default Action -To prevent the default action, call `e.preventDefault()` inside the callback: +### [`KeyboardStickyViewProps`](./keyboard-sticky-view) + +Inherits [KeyboardStickyViewProps](./keyboard-sticky-view). + +### `insets` + +An object containing `left` and `right` properties that define the `KeyboardToolbar` padding. This helps prevent overlap with system UI elements, especially in landscape orientation: ```tsx - { - e.preventDefault(); // the focus will not be moved to the prev input - }} -/> -``` +import { useSafeAreaInsets } from "react-native-safe-area-context"; -::: +// ... + +const insets = useSafeAreaInsets(); + +; +``` ### `opacity` @@ -268,10 +364,6 @@ This property allows to specify the opacity of the toolbar container. The value ``` -### `showArrows` - -A boolean prop indicating whether to show `next` and `prev` buttons. Can be useful to set it to `false` if you have only one input and want to show only `Done` button. Default to `true`. - ### `theme` Prop allowing you to specify the brand colors of your application for `KeyboardToolbar` component. If you want to re-use already platform specific colors you can import `DefaultKeyboardToolbarTheme` object and override colors only necessary colors: @@ -345,7 +437,11 @@ export default function ToolbarExample() { - + + + + + ); } @@ -444,6 +540,79 @@ const textInputStyles = StyleSheet.create({ For more comprehensive usage that covers more complex interactions please check [example](https://github.com/kirillzyusko/react-native-keyboard-controller/tree/main/example) app. ::: +## Migration to compound component + +To migrate from the legacy prop-based API to the compound API: + +1. Add elements that you want to render in the toolbar (e.g., `Prev`, `Next`, `Done`, `Content`, `Background`). + +```tsx +// Old: + + +// New: + + + + + +``` + +2. Move props like `content`, `blur`, `doneText` into dedicated sub-components: + +```tsx +// Old: +} blur={} doneText="Close" /> + +// New: + + + + + + + + + +``` + +3. If you used button callbacks, move them into dedicated sub-components: + +```tsx +// Old: + + +// New: + + + + + +``` + +4. If you used `showArrows` prop, move it into conditional rendering: + +```tsx +// Old: + + +// New: + + {showArrows ? : null} + {showArrows ? : null} + +``` + +:::info Struggle to migrate? + +If you found any bugs or inconsistent behavior comparing to old implementation and can not migrate to new compound API - don't hesitate to open an [issue](https://github.com/kirillzyusko/react-native-keyboard-controller/issues/new?assignees=kirillzyusko&labels=bug&template=bug_report.md&title=). It will help the project 🙏 + +::: + ## Limitations - By default `TextInput` search happens within `UIViewController`/`FragmentActivity` (current screen if you are using `react-native-screens`) diff --git a/example/src/screens/Examples/Toolbar/index.tsx b/example/src/screens/Examples/Toolbar/index.tsx index a8d9d2b324..c4b719da0b 100644 --- a/example/src/screens/Examples/Toolbar/index.tsx +++ b/example/src/screens/Examples/Toolbar/index.tsx @@ -156,18 +156,26 @@ function Form() { /> - ) : null - } insets={insets} opacity={Platform.OS === "ios" ? "4F" : "DD"} - onDoneCallback={haptic} - onNextCallback={haptic} - onPrevCallback={haptic} - /> + > + + + + + {showAutoFill ? ( + + ) : null} + + + + + ); } @@ -238,12 +246,3 @@ const styles = StyleSheet.create({ marginTop: 32, }, }); - -const blur = ( - -); diff --git a/src/components/KeyboardToolbar/compound/components/Background.tsx b/src/components/KeyboardToolbar/compound/components/Background.tsx new file mode 100644 index 0000000000..ea256fdb78 --- /dev/null +++ b/src/components/KeyboardToolbar/compound/components/Background.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +import type { ReactNode } from "react"; + +const Background: React.FC<{ children: ReactNode }> = ({ children }) => ( + <>{children} +); + +export default Background; diff --git a/src/components/KeyboardToolbar/compound/components/Content.tsx b/src/components/KeyboardToolbar/compound/components/Content.tsx new file mode 100644 index 0000000000..577c03232e --- /dev/null +++ b/src/components/KeyboardToolbar/compound/components/Content.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { StyleSheet, View } from "react-native"; + +import { TEST_ID_KEYBOARD_TOOLBAR_CONTENT } from "../../constants"; + +import type { ReactNode } from "react"; +import type { ViewProps } from "react-native"; + +const Content: React.FC = ({ + children, +}) => { + return ( + + {children} + + ); +}; + +const styles = StyleSheet.create({ + flex: { + flex: 1, + }, +}); + +export default Content; diff --git a/src/components/KeyboardToolbar/compound/components/Done.tsx b/src/components/KeyboardToolbar/compound/components/Done.tsx new file mode 100644 index 0000000000..bf9c2ad815 --- /dev/null +++ b/src/components/KeyboardToolbar/compound/components/Done.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import { useCallback, useMemo } from "react"; +import { StyleSheet, Text } from "react-native"; + +import { useKeyboardState } from "../../../../hooks"; +import { KeyboardController } from "../../../../module"; +import Button from "../../Button"; +import { TEST_ID_KEYBOARD_TOOLBAR_DONE } from "../../constants"; +import { useToolbarContext } from "../context"; + +import type { ButtonSubProps } from "./types"; +import type { ReactNode } from "react"; +import type { GestureResponderEvent } from "react-native"; + +const Done: React.FC & { text?: ReactNode }> = ({ + children, + onPress, + rippleRadius = 28, + text, + button: ButtonContainer = Button, +}) => { + const colorScheme = useKeyboardState((state) => state.appearance); + const context = useToolbarContext(); + const { theme } = context; + + const doneStyle = useMemo( + () => [styles.doneButton, { color: theme[colorScheme].primary }], + [colorScheme, theme], + ); + + const onPressDone = useCallback( + (event: GestureResponderEvent) => { + onPress?.(event); + + if (!event.isDefaultPrevented()) { + KeyboardController.dismiss(); + } + }, + [onPress], + ); + + return ( + + + {children ?? text ?? "Done"} + + + ); +}; + +const styles = StyleSheet.create({ + doneButton: { + fontWeight: "600", + fontSize: 15, + }, + doneButtonContainer: { + marginRight: 16, + marginLeft: 8, + }, +}); + +export default Done; diff --git a/src/components/KeyboardToolbar/compound/components/Next.tsx b/src/components/KeyboardToolbar/compound/components/Next.tsx new file mode 100644 index 0000000000..13243b625b --- /dev/null +++ b/src/components/KeyboardToolbar/compound/components/Next.tsx @@ -0,0 +1,55 @@ +import React, { useCallback } from "react"; + +import { KeyboardController } from "../../../../module"; +import Arrow from "../../Arrow"; +import Button from "../../Button"; +import { TEST_ID_KEYBOARD_TOOLBAR_NEXT } from "../../constants"; +import { useToolbarContext } from "../context"; + +import type { ButtonSubProps } from "./types"; +import type { GestureResponderEvent } from "react-native"; + +const Next: React.FC = ({ + children, + onPress, + disabled, + rippleRadius, + style, + button: ButtonContainer = Button, + icon: IconContainer = Arrow, +}) => { + const context = useToolbarContext(); + const { theme, isNextDisabled } = context; + + const isDisabled = disabled ?? isNextDisabled; + + const onPressNext = useCallback( + (event: GestureResponderEvent) => { + onPress?.(event); + + if (!event.isDefaultPrevented()) { + KeyboardController.setFocusTo("next"); + } + }, + [onPress], + ); + + return ( + + {children ?? ( + + )} + + ); +}; + +export default Next; diff --git a/src/components/KeyboardToolbar/compound/components/Prev.tsx b/src/components/KeyboardToolbar/compound/components/Prev.tsx new file mode 100644 index 0000000000..894944c573 --- /dev/null +++ b/src/components/KeyboardToolbar/compound/components/Prev.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { useCallback } from "react"; + +import { KeyboardController } from "../../../../module"; +import Arrow from "../../Arrow"; +import Button from "../../Button"; +import { TEST_ID_KEYBOARD_TOOLBAR_PREVIOUS } from "../../constants"; +import { useToolbarContext } from "../context"; + +import type { ButtonSubProps } from "./types"; +import type { GestureResponderEvent } from "react-native"; + +const Prev: React.FC = ({ + children, + onPress: onPressCallback, + disabled, + rippleRadius, + style, + button: ButtonContainer = Button, + icon: IconContainer = Arrow, +}) => { + const context = useToolbarContext(); + const { theme, isPrevDisabled } = context; + + const isDisabled = disabled ?? isPrevDisabled; + + const onPressPrev = useCallback( + (event: GestureResponderEvent) => { + onPressCallback?.(event); + + if (!event.isDefaultPrevented()) { + KeyboardController.setFocusTo("prev"); + } + }, + [onPressCallback], + ); + + return ( + + {children ?? ( + + )} + + ); +}; + +export default Prev; diff --git a/src/components/KeyboardToolbar/compound/components/index.ts b/src/components/KeyboardToolbar/compound/components/index.ts new file mode 100644 index 0000000000..82a324edb0 --- /dev/null +++ b/src/components/KeyboardToolbar/compound/components/index.ts @@ -0,0 +1,5 @@ +export { default as Background } from "./Background"; +export { default as Content } from "./Content"; +export { default as Done } from "./Done"; +export { default as Next } from "./Next"; +export { default as Prev } from "./Prev"; diff --git a/src/components/KeyboardToolbar/compound/components/types.ts b/src/components/KeyboardToolbar/compound/components/types.ts new file mode 100644 index 0000000000..9dcb6b5a13 --- /dev/null +++ b/src/components/KeyboardToolbar/compound/components/types.ts @@ -0,0 +1,15 @@ +import type Arrow from "../../Arrow"; +import type Button from "../../Button"; +import type { ReactNode } from "react"; +import type { GestureResponderEvent, ViewStyle } from "react-native"; + +export type ButtonSubProps = { + children?: ReactNode; + onPress?: (event: GestureResponderEvent) => void; + disabled?: boolean; + testID?: string; + rippleRadius?: number; + style?: ViewStyle; + button?: typeof Button; + icon?: typeof Arrow; +}; diff --git a/src/components/KeyboardToolbar/compound/context.ts b/src/components/KeyboardToolbar/compound/context.ts new file mode 100644 index 0000000000..6ac025bda7 --- /dev/null +++ b/src/components/KeyboardToolbar/compound/context.ts @@ -0,0 +1,25 @@ +import { createContext, useContext } from "react"; + +import type { KeyboardToolbarTheme } from "../types"; + +type ToolbarContextType = { + theme: KeyboardToolbarTheme; + isPrevDisabled: boolean; + isNextDisabled: boolean; +}; + +export const ToolbarContext = createContext( + undefined, +); + +export const useToolbarContext = () => { + const context = useContext(ToolbarContext); + + if (!context) { + throw new Error( + "KeyboardToolbar.* component must be used inside ", + ); + } + + return context; +}; diff --git a/src/components/KeyboardToolbar/index.tsx b/src/components/KeyboardToolbar/index.tsx index 741b230ef6..bc2a807fe7 100644 --- a/src/components/KeyboardToolbar/index.tsx +++ b/src/components/KeyboardToolbar/index.tsx @@ -1,80 +1,25 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { StyleSheet, Text, View } from "react-native"; +import React, { useEffect, useMemo, useState } from "react"; +import { StyleSheet, View } from "react-native"; import { FocusedInputEvents } from "../../bindings"; import { useKeyboardState } from "../../hooks"; -import { KeyboardController } from "../../module"; import KeyboardStickyView from "../KeyboardStickyView"; import Arrow from "./Arrow"; import Button from "./Button"; import { colors } from "./colors"; +import { Background, Content, Done, Next, Prev } from "./compound/components"; +import { ToolbarContext } from "./compound/context"; import { DEFAULT_OPACITY, KEYBOARD_HAS_ROUNDED_CORNERS, KEYBOARD_TOOLBAR_HEIGHT, OPENED_OFFSET, TEST_ID_KEYBOARD_TOOLBAR, - TEST_ID_KEYBOARD_TOOLBAR_CONTENT, - TEST_ID_KEYBOARD_TOOLBAR_DONE, - TEST_ID_KEYBOARD_TOOLBAR_NEXT, - TEST_ID_KEYBOARD_TOOLBAR_PREVIOUS, } from "./constants"; -import type { HEX, KeyboardToolbarTheme } from "./types"; -import type { KeyboardStickyViewProps } from "../KeyboardStickyView"; +import type { KeyboardToolbarProps } from "./types"; import type { ReactNode } from "react"; -import type { GestureResponderEvent, ViewProps } from "react-native"; - -type SafeAreaInsets = { - left: number; - right: number; -}; - -export type KeyboardToolbarProps = Omit< - ViewProps, - "style" | "testID" | "children" -> & { - /** An element that is shown in the middle of the toolbar. */ - content?: React.JSX.Element | null; - /** A set of dark/light colors consumed by toolbar component. */ - theme?: KeyboardToolbarTheme; - /** Custom text for done button. */ - doneText?: ReactNode; - /** Custom touchable component for toolbar (used for prev/next/done buttons). */ - button?: typeof Button; - /** Custom icon component used to display next/prev buttons. */ - icon?: typeof Arrow; - /** - * Whether to show next and previous buttons. Can be useful to set it to `false` if you have only one input - * and want to show only `Done` button. Default to `true`. - */ - showArrows?: boolean; - /** - * A callback that is called when the user presses the next button along with the default action. - */ - onNextCallback?: (event: GestureResponderEvent) => void; - /** - * A callback that is called when the user presses the previous button along with the default action. - */ - onPrevCallback?: (event: GestureResponderEvent) => void; - /** - * A callback that is called when the user presses the done button along with the default action. - */ - onDoneCallback?: (event: GestureResponderEvent) => void; - /** - * A component that applies blur effect to the toolbar. - */ - blur?: React.JSX.Element | null; - /** - * A value for container opacity in hexadecimal format (e.g. `ff`). Default value is `ff`. - */ - opacity?: HEX; - /** - * A object containing `left`/`right` properties. Used to specify proper container padding in landscape mode. - */ - insets?: SafeAreaInsets; -} & Pick; /** * `KeyboardToolbar` is a component that is shown above the keyboard with `Prev`/`Next` buttons from left and @@ -85,11 +30,20 @@ export type KeyboardToolbarProps = Omit< * @see {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-toolbar|Documentation} page for more details. * @example * ```tsx - * + * + * + * * ``` */ -const KeyboardToolbar: React.FC = (props) => { +const KeyboardToolbar: React.FC & { + Background: typeof Background; + Content: typeof Content; + Prev: typeof Prev; + Next: typeof Next; + Done: typeof Done; +} = (props) => { const { + children, content, theme = colors, doneText = "Done", @@ -113,6 +67,8 @@ const KeyboardToolbar: React.FC = (props) => { }); const isPrevDisabled = inputs.current === 0; const isNextDisabled = inputs.current === inputs.count - 1; + const buttonContainer = button ?? Button; + const iconContainer = icon ?? Arrow; useEffect(() => { const subscription = FocusedInputEvents.addListener("focusDidSet", (e) => { @@ -121,10 +77,6 @@ const KeyboardToolbar: React.FC = (props) => { return subscription.remove; }, []); - const doneStyle = useMemo( - () => [styles.doneButton, { color: theme[colorScheme].primary }], - [colorScheme, theme], - ); const toolbarStyle = useMemo( () => [ styles.toolbar, @@ -159,108 +111,98 @@ const KeyboardToolbar: React.FC = (props) => { }), [closed, opened], ); - const ButtonContainer = button || Button; - const IconContainer = icon || Arrow; - - const onPressNext = useCallback( - (event: GestureResponderEvent) => { - onNextCallback?.(event); - if (!event.isDefaultPrevented()) { - KeyboardController.setFocusTo("next"); + let backgroundElement: ReactNode = null; + let arrowsElement: ReactNode = null; + let contentContainer: ReactNode = null; + let doneElement: ReactNode = null; + + if (children) { + let prevChild: ReactNode = null; + let nextChild: ReactNode = null; + let contentChild: ReactNode = null; + let doneChild: ReactNode = null; + let backgroundChild: ReactNode = null; + + React.Children.forEach(children, (child) => { + if (!React.isValidElement(child)) { + return; } - }, - [onNextCallback], - ); - const onPressPrev = useCallback( - (event: GestureResponderEvent) => { - onPrevCallback?.(event); - - if (!event.isDefaultPrevented()) { - KeyboardController.setFocusTo("prev"); + const type = child.type; + + if (type === Background) { + backgroundChild = child; + } else if (type === Content) { + contentChild = child; + } else if (type === Prev) { + prevChild = child; + } else if (type === Next) { + nextChild = child; + } else if (type === Done) { + doneChild = child; } - }, - [onPrevCallback], - ); - const onPressDone = useCallback( - (event: GestureResponderEvent) => { - onDoneCallback?.(event); + }); - if (!event.isDefaultPrevented()) { - KeyboardController.dismiss(); - } - }, - [onDoneCallback], + backgroundElement = backgroundChild; + doneElement = doneChild; + arrowsElement = + prevChild || nextChild ? ( + + {prevChild} + {nextChild} + + ) : null; + contentContainer = contentChild ?? {contentChild}; + } else { + backgroundElement = blur; + arrowsElement = showArrows ? ( + + + + + ) : null; + contentContainer = {content}; + doneElement = doneText ? ( + + ) : null; + } + + const contextValue = useMemo( + () => ({ + theme, + isPrevDisabled, + isNextDisabled, + }), + [theme, isPrevDisabled, isNextDisabled], ); return ( - - - {blur} - {showArrows && ( - - - - - - - - - )} - - - {content} + + + + {backgroundElement} + {arrowsElement} + {contentContainer} + {doneElement} - {doneText && ( - - - {doneText} - - - )} - - + + ); }; const styles = StyleSheet.create({ - flex: { - flex: 1, - }, toolbar: { position: "absolute", bottom: 0, @@ -273,14 +215,6 @@ const styles = StyleSheet.create({ flexDirection: "row", paddingLeft: 8, }, - doneButton: { - fontWeight: "600", - fontSize: 15, - }, - doneButtonContainer: { - marginRight: 16, - marginLeft: 8, - }, floating: { alignSelf: "center", borderRadius: 20, @@ -288,5 +222,11 @@ const styles = StyleSheet.create({ }, }); -export { colors as DefaultKeyboardToolbarTheme }; +KeyboardToolbar.Background = Background; +KeyboardToolbar.Content = Content; +KeyboardToolbar.Prev = Prev; +KeyboardToolbar.Next = Next; +KeyboardToolbar.Done = Done; + +export { colors as DefaultKeyboardToolbarTheme, KeyboardToolbarProps }; export default KeyboardToolbar; diff --git a/src/components/KeyboardToolbar/types.ts b/src/components/KeyboardToolbar/types.ts index 2f0f659006..8164f22c00 100644 --- a/src/components/KeyboardToolbar/types.ts +++ b/src/components/KeyboardToolbar/types.ts @@ -1,13 +1,21 @@ -import type { ColorValue } from "react-native"; +import type Arrow from "./Arrow"; +import type Button from "./Button"; +import type { KeyboardStickyViewProps } from "../KeyboardStickyView"; +import type { ReactNode } from "react"; +import type { + ColorValue, + GestureResponderEvent, + ViewProps, +} from "react-native"; type Theme = { - /** Color for arrow when it's enabled */ + /** Color for arrow when it's enabled. */ primary: ColorValue; - /** Color for arrow when it's disabled */ + /** Color for arrow when it's disabled. */ disabled: ColorValue; - /** Keyboard toolbar background color */ + /** Keyboard toolbar background color. */ background: string; - /** Color for ripple effect (on button touch) on Android */ + /** Color for ripple effect (on button touch) on Android. */ ripple: ColorValue; }; export type KeyboardToolbarTheme = { @@ -38,3 +46,91 @@ type HexSymbol = | "e" | "f"; export type HEX = `${HexSymbol}${HexSymbol}`; + +type SafeAreaInsets = { + left: number; + right: number; +}; + +export type KeyboardToolbarProps = Omit< + ViewProps, + "style" | "testID" | "children" +> & { + /** + * An element that is shown in the middle of the toolbar. + * + * @deprecated Use compound API with `` component instead. + */ + content?: React.JSX.Element | null; + /** A set of dark/light colors consumed by toolbar component. */ + theme?: KeyboardToolbarTheme; + /** + * Custom text for done button. + * + * @deprecated Use compound API with `` component and `text` prop instead. + */ + doneText?: ReactNode; + /** + * Custom touchable component for toolbar (used for prev/next/done buttons). + * + * @deprecated Use `button` property for corresponding element instead: + * ```tsx + * + * + * + * ```. + */ + button?: typeof Button; + /** + * Custom icon component used to display next/prev buttons. + * + * @deprecated Use `icon` property for corresponding element instead: + * ```tsx + * + * + * + * ```. + */ + icon?: typeof Arrow; + /** + * Whether to show next and previous buttons. Can be useful to set it to `false` if you have only one input + * and want to show only `Done` button. Default to `true`. + * + * @deprecated Use compound API and conditional rendering for `` and ``. + */ + showArrows?: boolean; + /** + * A callback that is called when the user presses the next button along with the default action. + * + * @deprecated Use compound API with `` and `onPress` callback instead. + */ + onNextCallback?: (event: GestureResponderEvent) => void; + /** + * A callback that is called when the user presses the previous button along with the default action. + * + * @deprecated Use compound API with `` and `onPress` callback instead. + */ + onPrevCallback?: (event: GestureResponderEvent) => void; + /** + * A callback that is called when the user presses the done button along with the default action. + * + * @deprecated Use compound API with `` and `onPress` callback instead. + */ + onDoneCallback?: (event: GestureResponderEvent) => void; + /** + * A component that applies blur effect to the toolbar. + * + * @deprecated Use compound API and `` instead. + */ + blur?: React.JSX.Element | null; + /** + * A value for container opacity in hexadecimal format (e.g. `ff`). Default value is `ff`. + */ + opacity?: HEX; + /** + * A object containing `left`/`right` properties. Used to specify proper container padding in landscape mode. + */ + insets?: SafeAreaInsets; + /** JSX children in case if compound API is used. */ + children?: ReactNode; +} & Pick;