Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ccf36e4

Browse files
committedApr 12, 2023
Implement PickerAvoidingView and PickerStateProvider
...which provide tools for ensuring that a picker is not covered by its own modal on iOS.
1 parent 84ee97d commit ccf36e4

File tree

6 files changed

+109
-5
lines changed

6 files changed

+109
-5
lines changed
 

‎index.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,11 @@ declare class Picker extends React.Component<PickerSelectProps> {
9292
}
9393

9494
export default Picker;
95+
96+
type PickerStateProviderProps = {
97+
readonly children: React.ReactChild;
98+
};
99+
100+
export const PickerStateProvider: React.ComponentType<PickerStateProviderProps>;
101+
102+
export const PickerAvoidingView: React.ComponentType<React.PropsWithChildren>;

‎src/PickerAvoidingView/index.ios.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import { StyleSheet, View } from 'react-native';
3+
import { PickerStateContext } from "../PickerStateProvider";
4+
import { IOS_MODAL_HEIGHT } from "../constants";
5+
6+
const styles = {
7+
container: {
8+
flexDirection: 'column',
9+
justifyContent: 'flex-start',
10+
alignItems: 'stretch',
11+
},
12+
contentContainer: {
13+
flex: 1,
14+
},
15+
space: {
16+
height: IOS_MODAL_HEIGHT,
17+
},
18+
};
19+
20+
/**
21+
* PickerAvoidingView is a React component that adjusts the view layout to avoid
22+
* being covered by an open iOS UIPickerView modal. It's meant to be similar to
23+
* the built-in KeyboardAvoidingView component, but specifically tailored for
24+
* iOS picker modals.
25+
*
26+
* In order for this component to work correctly, all the pickers and the
27+
* PickerAvoidingView should have a PickerStateProvider ancestor.
28+
*
29+
* @param {React.ReactNode} props.children - The child components that should be
30+
* protected from obstruction by the picker modal
31+
*/
32+
export function PickerAvoidingView(props) {
33+
const context = React.useContext(PickerStateContext);
34+
const isPickerOpen = context && context.isPickerOpen;
35+
36+
return (
37+
<View style={StyleSheet.compose(props.style, styles.container)}>
38+
<View style={styles.contentContainer}>
39+
{props.children}
40+
</View>
41+
{isPickerOpen && <View style={styles.space}/>}
42+
</View>
43+
);
44+
}

‎src/PickerAvoidingView/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import { View } from "react-native";
3+
4+
/**
5+
* As, currently, only on iOS the picker's modal resembles the software keyboard
6+
* in any way, the default implementation doesn't have any avoiding logic.
7+
*
8+
* @param {React.ReactNode} props.children - The child components to render
9+
* within the PickerAvoidingView.
10+
*/
11+
export function PickerAvoidingView(props) {
12+
return <View {...props}>{props.children}</View>;
13+
}

‎src/PickerStateProvider.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
3+
/**
4+
* @typedef {Object} PickerStateData
5+
* @property {boolean} isPickerOpen - Indicates whether any picker is currently open
6+
*
7+
* PickerStateContext is a context that gives access to PickerStateData.
8+
*/
9+
export const PickerStateContext = React.createContext();
10+
11+
/**
12+
* PickerStateProvider provides PickerStateContext and manages the necessary
13+
* state.
14+
*
15+
* This component should be used as a single top-level provider for all picker
16+
* instances in your application.
17+
*/
18+
export function PickerStateProvider(props) {
19+
const [isPickerOpen, setIsPickerOpen] = React.useState(false);
20+
21+
const context = {
22+
isPickerOpen,
23+
setIsPickerOpen,
24+
};
25+
26+
return (
27+
<PickerStateContext.Provider value={context}>
28+
{props.children}
29+
</PickerStateContext.Provider>
30+
);
31+
}

‎src/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Measuring the modal before rendering is not working reliably, so we need to hardcode the height
2+
// This height was tested thoroughly on several iPhone models (iPhone SE, from iPhone 8 to 14 Pro, and 14 Pro Max)
3+
export const IOS_MODAL_HEIGHT = 262;

‎src/index.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import isEqual from 'lodash.isequal';
55
import { Picker } from '@react-native-picker/picker';
66
import { defaultStyles } from './styles';
77
import { Dimensions } from 'react-native';
8-
9-
// Measuring the modal before rendering is not working reliably, so we need to hardcode the height
10-
// This height was tested thoroughly on several iPhone Models (from iPhone 8 to 14 Pro)
11-
const IOS_MODAL_HEIGHT = 262;
8+
import { PickerAvoidingView } from './PickerAvoidingView';
9+
import { PickerStateContext, PickerStateProvider } from './PickerStateProvider';
10+
import { IOS_MODAL_HEIGHT } from './constants';
1211

1312
export default class RNPickerSelect extends PureComponent {
13+
static contextType = PickerStateContext;
14+
1415
static propTypes = {
1516
onValueChange: PropTypes.func.isRequired,
1617
items: PropTypes.arrayOf(
@@ -275,6 +276,10 @@ export default class RNPickerSelect extends PureComponent {
275276
const animationType =
276277
modalProps && modalProps.animationType ? modalProps.animationType : 'slide';
277278

279+
if (this.context) {
280+
this.context.setIsPickerOpen(!showPicker);
281+
}
282+
278283
this.triggerOpenCloseCallbacks();
279284

280285
this.setState(
@@ -617,4 +622,4 @@ export default class RNPickerSelect extends PureComponent {
617622
}
618623
}
619624

620-
export { defaultStyles };
625+
export { defaultStyles, PickerStateProvider, PickerAvoidingView };

0 commit comments

Comments
 (0)