react-native-bottom-safe-area 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/lib/commonjs/index.js +91 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/index.js +83 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/index.d.ts +41 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/package.json +89 -0
- package/src/index.tsx +140 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# react-native-bottom-safe-area
|
|
2
|
+
|
|
3
|
+
A lightweight, zero-native-dependency React Native library that creates a bottom container which automatically adapts to Safe Area insets (iPhone X+ Home Indicator) and Keyboard visibility.
|
|
4
|
+
|
|
5
|
+
Perfect for "Continue", "Submit", or "Checkout" buttons that need to sit at the bottom of the screen but never get hidden behind the keyboard or system navigation.
|
|
6
|
+
|
|
7
|
+
## Why this exists?
|
|
8
|
+
|
|
9
|
+
Even with `react-native-safe-area-context`, handling the keyboard interactions along with safe area insets often leads to:
|
|
10
|
+
- Boilerplate code in every screen.
|
|
11
|
+
- Buttons jumping or getting hidden behind the keyboard.
|
|
12
|
+
- Double padding issues (Safe Area + Keyboard Cushion).
|
|
13
|
+
- Manual calculations for different devices.
|
|
14
|
+
|
|
15
|
+
This library encapsulates that logic into a single simple component wrapping your button.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- 🚀 **Auto-handling**: Automatically applies `paddingBottom` based on Safe Area or Keyboard height.
|
|
20
|
+
- ⌨️ **Keyboard Aware**: Smoothly animates padding when keyboard opens/closes.
|
|
21
|
+
- 📱 **Cross Platform**: Works on iOS (Home Indicator) and Android (Navigation Bar).
|
|
22
|
+
- ⚡ **Lightweight**: No native code to link, just pure JS/TS + `react-native-safe-area-context`.
|
|
23
|
+
- 🛡️ **TypeScript**: Fully typed.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install react-native-bottom-safe-area
|
|
29
|
+
# OR
|
|
30
|
+
yarn add react-native-bottom-safe-area
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This library requires `react-native-safe-area-context` as a peer dependency. If you haven't installed it yet:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install react-native-safe-area-context
|
|
37
|
+
cd ios && pod install && cd ..
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
Wrap your functionality (like a Button) with `BottomSafeArea`. It acts as a View that adds dynamic bottom padding.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import React from 'react';
|
|
46
|
+
import { Button, View, Text } from 'react-native';
|
|
47
|
+
import { BottomSafeArea } from 'react-native-bottom-safe-area';
|
|
48
|
+
|
|
49
|
+
export default function MyScreen() {
|
|
50
|
+
return (
|
|
51
|
+
<View style={{ flex: 1, justifyContent: 'space-between' }}>
|
|
52
|
+
<View style={{ padding: 20 }}>
|
|
53
|
+
<Text>Some content here...</Text>
|
|
54
|
+
</View>
|
|
55
|
+
|
|
56
|
+
{/* Place this at the bottom of your container */}
|
|
57
|
+
<BottomSafeArea extraSpacing={10} backgroundColor="white">
|
|
58
|
+
<Button title="Continue" onPress={() => console.log('Pressed')} />
|
|
59
|
+
</BottomSafeArea>
|
|
60
|
+
</View>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### `<BottomSafeArea />`
|
|
68
|
+
|
|
69
|
+
| Prop | Type | Default | Description |
|
|
70
|
+
|------|------|---------|-------------|
|
|
71
|
+
| `children` | `ReactNode` | **Required** | The content to render (Buttons, Text, etc.). |
|
|
72
|
+
| `extraSpacing` | `number` | `0` | Additional padding (in pixels) to add on top of the calculated safe area/keyboard offset. |
|
|
73
|
+
| `backgroundColor` | `string` | `'transparent'` | Background color of the container view. |
|
|
74
|
+
| `style` | `ViewStyle` | `undefined` | Custom styles for the container view. |
|
|
75
|
+
|
|
76
|
+
## Android Parsing
|
|
77
|
+
|
|
78
|
+
On Android, ensure your `android/app/src/main/AndroidManifest.xml` has `windowSoftInputMode="adjustResize"` for best results, although this library attempts to handle padding manually as well.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.BottomSafeArea = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
|
|
10
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
11
|
+
if (_reactNative.Platform.OS === 'android' && _reactNative.UIManager.setLayoutAnimationEnabledExperimental) {
|
|
12
|
+
_reactNative.UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Props for the BottomSafeArea component.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A component that automatically handles bottom safe area insets and keyboard avoidance.
|
|
21
|
+
*
|
|
22
|
+
* It listens to keyboard events and adjusts the bottom padding accordingly,
|
|
23
|
+
* ensuring that the content (e.g., action buttons) is always visible and clickable,
|
|
24
|
+
* never hidden behind the keyboard or the home indicator.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <BottomSafeArea>
|
|
28
|
+
* <Button title="Continue" onPress={...} />
|
|
29
|
+
* </BottomSafeArea>
|
|
30
|
+
*/
|
|
31
|
+
const BottomSafeArea = ({
|
|
32
|
+
children,
|
|
33
|
+
extraSpacing = 0,
|
|
34
|
+
backgroundColor = 'transparent',
|
|
35
|
+
style
|
|
36
|
+
}) => {
|
|
37
|
+
const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
|
|
38
|
+
const [bottomPadding, setBottomPadding] = (0, _react.useState)(0);
|
|
39
|
+
(0, _react.useEffect)(() => {
|
|
40
|
+
// Function to handle keyboard showing
|
|
41
|
+
const onKeyboardShow = e => {
|
|
42
|
+
// Configure animation for smooth transition
|
|
43
|
+
_reactNative.LayoutAnimation.configureNext(_reactNative.LayoutAnimation.Presets.easeInEaseOut);
|
|
44
|
+
|
|
45
|
+
// When keyboard shows, we want to pad by the keyboard height
|
|
46
|
+
// However, on iOS, the keyboard sits on top of the view, so we might need to adjust logic based on where this component is placed.
|
|
47
|
+
// But typically for a bottom-anchored view, we want to lift it up.
|
|
48
|
+
// The `e.endCoordinates.height` gives the keyboard height.
|
|
49
|
+
// We subtract insets.bottom because the keyboard covers the safe area too.
|
|
50
|
+
// If we are already respecting safe area, we don't want to double count it when keyboard appears.
|
|
51
|
+
|
|
52
|
+
const keyboardHeight = e.endCoordinates.height;
|
|
53
|
+
if (_reactNative.Platform.OS === 'ios') {
|
|
54
|
+
setBottomPadding(keyboardHeight);
|
|
55
|
+
} else {
|
|
56
|
+
setBottomPadding(keyboardHeight);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Function to handle keyboard hiding
|
|
61
|
+
const onKeyboardHide = () => {
|
|
62
|
+
_reactNative.LayoutAnimation.configureNext(_reactNative.LayoutAnimation.Presets.easeInEaseOut);
|
|
63
|
+
setBottomPadding(0);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Subscriptions
|
|
67
|
+
const showSubscription = _reactNative.Keyboard.addListener(_reactNative.Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', onKeyboardShow);
|
|
68
|
+
const hideSubscription = _reactNative.Keyboard.addListener(_reactNative.Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', onKeyboardHide);
|
|
69
|
+
return () => {
|
|
70
|
+
showSubscription.remove();
|
|
71
|
+
hideSubscription.remove();
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
const finalBottomPadding = Math.max(bottomPadding, insets.bottom) + extraSpacing;
|
|
75
|
+
return /*#__PURE__*/_react.default.createElement(_reactNative.View, {
|
|
76
|
+
style: [styles.container, {
|
|
77
|
+
paddingBottom: finalBottomPadding,
|
|
78
|
+
backgroundColor: backgroundColor
|
|
79
|
+
}, style]
|
|
80
|
+
}, children);
|
|
81
|
+
};
|
|
82
|
+
exports.BottomSafeArea = BottomSafeArea;
|
|
83
|
+
const styles = _reactNative.StyleSheet.create({
|
|
84
|
+
container: {
|
|
85
|
+
// Default style: full width, acting as a container at the bottom
|
|
86
|
+
width: '100%'
|
|
87
|
+
// We don't enforce absolute positioning here to allow flexibility,
|
|
88
|
+
// but often this component is used at the bottom of a flex:1 container.
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_reactNativeSafeAreaContext","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","Platform","OS","UIManager","setLayoutAnimationEnabledExperimental","BottomSafeArea","children","extraSpacing","backgroundColor","style","insets","useSafeAreaInsets","bottomPadding","setBottomPadding","useState","useEffect","onKeyboardShow","LayoutAnimation","configureNext","Presets","easeInEaseOut","keyboardHeight","endCoordinates","height","onKeyboardHide","showSubscription","Keyboard","addListener","hideSubscription","remove","finalBottomPadding","Math","max","bottom","createElement","View","styles","container","paddingBottom","exports","StyleSheet","create","width"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View, StyleSheet, Keyboard, Platform, LayoutAnimation, UIManager } from 'react-native';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
|
5
|
+
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Props for the BottomSafeArea component.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A component that automatically handles bottom safe area insets and keyboard avoidance.
|
|
14
|
+
*
|
|
15
|
+
* It listens to keyboard events and adjusts the bottom padding accordingly,
|
|
16
|
+
* ensuring that the content (e.g., action buttons) is always visible and clickable,
|
|
17
|
+
* never hidden behind the keyboard or the home indicator.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* <BottomSafeArea>
|
|
21
|
+
* <Button title="Continue" onPress={...} />
|
|
22
|
+
* </BottomSafeArea>
|
|
23
|
+
*/
|
|
24
|
+
export const BottomSafeArea = ({
|
|
25
|
+
children,
|
|
26
|
+
extraSpacing = 0,
|
|
27
|
+
backgroundColor = 'transparent',
|
|
28
|
+
style
|
|
29
|
+
}) => {
|
|
30
|
+
const insets = useSafeAreaInsets();
|
|
31
|
+
const [bottomPadding, setBottomPadding] = useState(0);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
// Function to handle keyboard showing
|
|
34
|
+
const onKeyboardShow = e => {
|
|
35
|
+
// Configure animation for smooth transition
|
|
36
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
37
|
+
|
|
38
|
+
// When keyboard shows, we want to pad by the keyboard height
|
|
39
|
+
// However, on iOS, the keyboard sits on top of the view, so we might need to adjust logic based on where this component is placed.
|
|
40
|
+
// But typically for a bottom-anchored view, we want to lift it up.
|
|
41
|
+
// The `e.endCoordinates.height` gives the keyboard height.
|
|
42
|
+
// We subtract insets.bottom because the keyboard covers the safe area too.
|
|
43
|
+
// If we are already respecting safe area, we don't want to double count it when keyboard appears.
|
|
44
|
+
|
|
45
|
+
const keyboardHeight = e.endCoordinates.height;
|
|
46
|
+
if (Platform.OS === 'ios') {
|
|
47
|
+
setBottomPadding(keyboardHeight);
|
|
48
|
+
} else {
|
|
49
|
+
setBottomPadding(keyboardHeight);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Function to handle keyboard hiding
|
|
54
|
+
const onKeyboardHide = () => {
|
|
55
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
56
|
+
setBottomPadding(0);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Subscriptions
|
|
60
|
+
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', onKeyboardShow);
|
|
61
|
+
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', onKeyboardHide);
|
|
62
|
+
return () => {
|
|
63
|
+
showSubscription.remove();
|
|
64
|
+
hideSubscription.remove();
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
const finalBottomPadding = Math.max(bottomPadding, insets.bottom) + extraSpacing;
|
|
68
|
+
return /*#__PURE__*/React.createElement(View, {
|
|
69
|
+
style: [styles.container, {
|
|
70
|
+
paddingBottom: finalBottomPadding,
|
|
71
|
+
backgroundColor: backgroundColor
|
|
72
|
+
}, style]
|
|
73
|
+
}, children);
|
|
74
|
+
};
|
|
75
|
+
const styles = StyleSheet.create({
|
|
76
|
+
container: {
|
|
77
|
+
// Default style: full width, acting as a container at the bottom
|
|
78
|
+
width: '100%'
|
|
79
|
+
// We don't enforce absolute positioning here to allow flexibility,
|
|
80
|
+
// but often this component is used at the bottom of a flex:1 container.
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","useEffect","useState","View","StyleSheet","Keyboard","Platform","LayoutAnimation","UIManager","useSafeAreaInsets","OS","setLayoutAnimationEnabledExperimental","BottomSafeArea","children","extraSpacing","backgroundColor","style","insets","bottomPadding","setBottomPadding","onKeyboardShow","e","configureNext","Presets","easeInEaseOut","keyboardHeight","endCoordinates","height","onKeyboardHide","showSubscription","addListener","hideSubscription","remove","finalBottomPadding","Math","max","bottom","createElement","styles","container","paddingBottom","create","width"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"AAAA,OAAOA,KAAK,IAAIC,SAAS,EAAEC,QAAQ,QAAmB,OAAO;AAC7D,SACIC,IAAI,EACJC,UAAU,EACVC,QAAQ,EACRC,QAAQ,EACRC,eAAe,EAGfC,SAAS,QACN,cAAc;AACrB,SAASC,iBAAiB,QAAQ,gCAAgC;AAElE,IACIH,QAAQ,CAACI,EAAE,KAAK,SAAS,IACzBF,SAAS,CAACG,qCAAqC,EACjD;EACEH,SAAS,CAACG,qCAAqC,CAAC,IAAI,CAAC;AACzD;;AAEA;AACA;AACA;;AA2BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,cAAc,GAAGA,CAAC;EAC3BC,QAAQ;EACRC,YAAY,GAAG,CAAC;EAChBC,eAAe,GAAG,aAAa;EAC/BC;AACiB,CAAC,KAAK;EACvB,MAAMC,MAAM,GAAGR,iBAAiB,CAAC,CAAC;EAClC,MAAM,CAACS,aAAa,EAAEC,gBAAgB,CAAC,GAAGjB,QAAQ,CAAC,CAAC,CAAC;EAErDD,SAAS,CAAC,MAAM;IACZ;IACA,MAAMmB,cAAc,GAAIC,CAAgB,IAAK;MACzC;MACAd,eAAe,CAACe,aAAa,CAACf,eAAe,CAACgB,OAAO,CAACC,aAAa,CAAC;;MAEpE;MACA;MACA;MACA;MACA;MACA;;MAEA,MAAMC,cAAc,GAAGJ,CAAC,CAACK,cAAc,CAACC,MAAM;MAE9C,IAAIrB,QAAQ,CAACI,EAAE,KAAK,KAAK,EAAE;QACvBS,gBAAgB,CAACM,cAAc,CAAC;MACpC,CAAC,MAAM;QACHN,gBAAgB,CAACM,cAAc,CAAC;MACpC;IACJ,CAAC;;IAED;IACA,MAAMG,cAAc,GAAGA,CAAA,KAAM;MACzBrB,eAAe,CAACe,aAAa,CAACf,eAAe,CAACgB,OAAO,CAACC,aAAa,CAAC;MACpEL,gBAAgB,CAAC,CAAC,CAAC;IACvB,CAAC;;IAED;IACA,MAAMU,gBAAgB,GAAGxB,QAAQ,CAACyB,WAAW,CACzCxB,QAAQ,CAACI,EAAE,KAAK,KAAK,GAAG,kBAAkB,GAAG,iBAAiB,EAC9DU,cACJ,CAAC;IACD,MAAMW,gBAAgB,GAAG1B,QAAQ,CAACyB,WAAW,CACzCxB,QAAQ,CAACI,EAAE,KAAK,KAAK,GAAG,kBAAkB,GAAG,iBAAiB,EAC9DkB,cACJ,CAAC;IAED,OAAO,MAAM;MACTC,gBAAgB,CAACG,MAAM,CAAC,CAAC;MACzBD,gBAAgB,CAACC,MAAM,CAAC,CAAC;IAC7B,CAAC;EACL,CAAC,EAAE,EAAE,CAAC;EAEN,MAAMC,kBAAkB,GAAGC,IAAI,CAACC,GAAG,CAACjB,aAAa,EAAED,MAAM,CAACmB,MAAM,CAAC,GAAGtB,YAAY;EAEhF,oBACId,KAAA,CAAAqC,aAAA,CAAClC,IAAI;IACDa,KAAK,EAAE,CACHsB,MAAM,CAACC,SAAS,EAChB;MACIC,aAAa,EAAEP,kBAAkB;MACjClB,eAAe,EAAEA;IACrB,CAAC,EACDC,KAAK;EACP,GAEDH,QACC,CAAC;AAEf,CAAC;AAED,MAAMyB,MAAM,GAAGlC,UAAU,CAACqC,MAAM,CAAC;EAC7BF,SAAS,EAAE;IACP;IACAG,KAAK,EAAE;IACP;IACA;EACJ;AACJ,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { ViewStyle } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* Props for the BottomSafeArea component.
|
|
5
|
+
*/
|
|
6
|
+
export interface BottomSafeAreaProps {
|
|
7
|
+
/**
|
|
8
|
+
* The content to render inside the safe area container.
|
|
9
|
+
* Typically a button or a row of buttons.
|
|
10
|
+
*/
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Additional spacing (padding) to add to the bottom, in pixels.
|
|
14
|
+
* Useful if you want more space than just the safe area/keyboard height.
|
|
15
|
+
* Defaults to 0.
|
|
16
|
+
*/
|
|
17
|
+
extraSpacing?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Background color of the safe area container.
|
|
20
|
+
* Defaults to 'transparent'.
|
|
21
|
+
*/
|
|
22
|
+
backgroundColor?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional custom style for the container.
|
|
25
|
+
*/
|
|
26
|
+
style?: ViewStyle;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A component that automatically handles bottom safe area insets and keyboard avoidance.
|
|
30
|
+
*
|
|
31
|
+
* It listens to keyboard events and adjusts the bottom padding accordingly,
|
|
32
|
+
* ensuring that the content (e.g., action buttons) is always visible and clickable,
|
|
33
|
+
* never hidden behind the keyboard or the home indicator.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* <BottomSafeArea>
|
|
37
|
+
* <Button title="Continue" onPress={...} />
|
|
38
|
+
* </BottomSafeArea>
|
|
39
|
+
*/
|
|
40
|
+
export declare const BottomSafeArea: ({ children, extraSpacing, backgroundColor, style, }: BottomSafeAreaProps) => React.JSX.Element;
|
|
41
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuB,SAAS,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,EAMH,SAAS,EAGZ,MAAM,cAAc,CAAC;AAUtB;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,GAAI,qDAK5B,mBAAmB,sBAgErB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-bottom-safe-area",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A reusable BottomSafeArea component that handles safe area insets and keyboard avoidance automatically.",
|
|
5
|
+
"main": "lib/commonjs/index.js",
|
|
6
|
+
"module": "lib/module/index.js",
|
|
7
|
+
"types": "lib/typescript/index.d.ts",
|
|
8
|
+
"react-native": "src/index.tsx",
|
|
9
|
+
"source": "src/index.tsx",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"lib",
|
|
13
|
+
"android",
|
|
14
|
+
"ios",
|
|
15
|
+
"cpp",
|
|
16
|
+
"*.podspec",
|
|
17
|
+
"!lib/typescript/example",
|
|
18
|
+
"!android/build",
|
|
19
|
+
"!ios/build",
|
|
20
|
+
"!**/__tests__",
|
|
21
|
+
"!**/__fixtures__",
|
|
22
|
+
"!**/__mocks__"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
26
|
+
"typescript": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
28
|
+
"prepare": "bob build",
|
|
29
|
+
"release": "release-it",
|
|
30
|
+
"build": "bob build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"react-native",
|
|
34
|
+
"ios",
|
|
35
|
+
"android",
|
|
36
|
+
"safe-area",
|
|
37
|
+
"keyboard",
|
|
38
|
+
"bottom-sheet",
|
|
39
|
+
"ui"
|
|
40
|
+
],
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/user/react-native-bottom-safe-area.git"
|
|
44
|
+
},
|
|
45
|
+
"author": "Sakshi Bheda <sakshibheda@example.com> (https://github.com/sakshibheda)",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/user/react-native-bottom-safe-area/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/user/react-native-bottom-safe-area#readme",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"registry": "https://registry.npmjs.org/"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@react-native/eslint-config": "^0.73.1",
|
|
56
|
+
"@types/jest": "^29.5.5",
|
|
57
|
+
"@types/react": "^18.2.44",
|
|
58
|
+
"eslint": "^8.51.0",
|
|
59
|
+
"prettier": "^3.0.3",
|
|
60
|
+
"react": "18.2.0",
|
|
61
|
+
"react-native": "^0.73.4",
|
|
62
|
+
"react-native-builder-bob": "^0.23.2",
|
|
63
|
+
"react-native-safe-area-context": "^4.9.0",
|
|
64
|
+
"release-it": "^17.0.0",
|
|
65
|
+
"typescript": "^5.2.2"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"react": "*",
|
|
69
|
+
"react-native": "*",
|
|
70
|
+
"react-native-safe-area-context": "*"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">= 18.0.0"
|
|
74
|
+
},
|
|
75
|
+
"react-native-builder-bob": {
|
|
76
|
+
"source": "src",
|
|
77
|
+
"output": "lib",
|
|
78
|
+
"targets": [
|
|
79
|
+
"commonjs",
|
|
80
|
+
"module",
|
|
81
|
+
[
|
|
82
|
+
"typescript",
|
|
83
|
+
{
|
|
84
|
+
"project": "tsconfig.json"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { useEffect, useState, ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Keyboard,
|
|
6
|
+
Platform,
|
|
7
|
+
LayoutAnimation,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
KeyboardEvent,
|
|
10
|
+
UIManager,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
Platform.OS === 'android' &&
|
|
16
|
+
UIManager.setLayoutAnimationEnabledExperimental
|
|
17
|
+
) {
|
|
18
|
+
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props for the BottomSafeArea component.
|
|
23
|
+
*/
|
|
24
|
+
export interface BottomSafeAreaProps {
|
|
25
|
+
/**
|
|
26
|
+
* The content to render inside the safe area container.
|
|
27
|
+
* Typically a button or a row of buttons.
|
|
28
|
+
*/
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Additional spacing (padding) to add to the bottom, in pixels.
|
|
33
|
+
* Useful if you want more space than just the safe area/keyboard height.
|
|
34
|
+
* Defaults to 0.
|
|
35
|
+
*/
|
|
36
|
+
extraSpacing?: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Background color of the safe area container.
|
|
40
|
+
* Defaults to 'transparent'.
|
|
41
|
+
*/
|
|
42
|
+
backgroundColor?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Optional custom style for the container.
|
|
46
|
+
*/
|
|
47
|
+
style?: ViewStyle;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A component that automatically handles bottom safe area insets and keyboard avoidance.
|
|
52
|
+
*
|
|
53
|
+
* It listens to keyboard events and adjusts the bottom padding accordingly,
|
|
54
|
+
* ensuring that the content (e.g., action buttons) is always visible and clickable,
|
|
55
|
+
* never hidden behind the keyboard or the home indicator.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* <BottomSafeArea>
|
|
59
|
+
* <Button title="Continue" onPress={...} />
|
|
60
|
+
* </BottomSafeArea>
|
|
61
|
+
*/
|
|
62
|
+
export const BottomSafeArea = ({
|
|
63
|
+
children,
|
|
64
|
+
extraSpacing = 0,
|
|
65
|
+
backgroundColor = 'transparent',
|
|
66
|
+
style,
|
|
67
|
+
}: BottomSafeAreaProps) => {
|
|
68
|
+
const insets = useSafeAreaInsets();
|
|
69
|
+
const [bottomPadding, setBottomPadding] = useState(0);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
// Function to handle keyboard showing
|
|
73
|
+
const onKeyboardShow = (e: KeyboardEvent) => {
|
|
74
|
+
// Configure animation for smooth transition
|
|
75
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
76
|
+
|
|
77
|
+
// When keyboard shows, we want to pad by the keyboard height
|
|
78
|
+
// However, on iOS, the keyboard sits on top of the view, so we might need to adjust logic based on where this component is placed.
|
|
79
|
+
// But typically for a bottom-anchored view, we want to lift it up.
|
|
80
|
+
// The `e.endCoordinates.height` gives the keyboard height.
|
|
81
|
+
// We subtract insets.bottom because the keyboard covers the safe area too.
|
|
82
|
+
// If we are already respecting safe area, we don't want to double count it when keyboard appears.
|
|
83
|
+
|
|
84
|
+
const keyboardHeight = e.endCoordinates.height;
|
|
85
|
+
|
|
86
|
+
if (Platform.OS === 'ios') {
|
|
87
|
+
setBottomPadding(keyboardHeight);
|
|
88
|
+
} else {
|
|
89
|
+
setBottomPadding(keyboardHeight);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Function to handle keyboard hiding
|
|
94
|
+
const onKeyboardHide = () => {
|
|
95
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
96
|
+
setBottomPadding(0);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Subscriptions
|
|
100
|
+
const showSubscription = Keyboard.addListener(
|
|
101
|
+
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
|
|
102
|
+
onKeyboardShow
|
|
103
|
+
);
|
|
104
|
+
const hideSubscription = Keyboard.addListener(
|
|
105
|
+
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
|
|
106
|
+
onKeyboardHide
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
showSubscription.remove();
|
|
111
|
+
hideSubscription.remove();
|
|
112
|
+
};
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const finalBottomPadding = Math.max(bottomPadding, insets.bottom) + extraSpacing;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<View
|
|
119
|
+
style={[
|
|
120
|
+
styles.container,
|
|
121
|
+
{
|
|
122
|
+
paddingBottom: finalBottomPadding,
|
|
123
|
+
backgroundColor: backgroundColor,
|
|
124
|
+
},
|
|
125
|
+
style,
|
|
126
|
+
]}
|
|
127
|
+
>
|
|
128
|
+
{children}
|
|
129
|
+
</View>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
container: {
|
|
135
|
+
// Default style: full width, acting as a container at the bottom
|
|
136
|
+
width: '100%',
|
|
137
|
+
// We don't enforce absolute positioning here to allow flexibility,
|
|
138
|
+
// but often this component is used at the bottom of a flex:1 container.
|
|
139
|
+
},
|
|
140
|
+
});
|