Skip to content

Commit 683fd45

Browse files
committed
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 683fd45

File tree

6 files changed

+108
-5
lines changed

6 files changed

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

src/PickerAvoidingView/index.js

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

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 (from iPhone 8 to 14 Pro)
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)