Skip to content

Commit 8d32015

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 dabfcd5 commit 8d32015

File tree

7 files changed

+96
-6
lines changed

7 files changed

+96
-6
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>;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"prettier": "^2.0.5",
4242
"pretty-quick": "^2.0.1",
4343
"prop-types": "^15.7.2",
44-
"react": "16.6.1",
44+
"react": "^16.8.6",
4545
"react-dom": "^16.6.1",
4646
"react-native": "0.57.7",
4747
"react-test-renderer": "^16.6.1"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
/**
7+
* PickerAvoidingView is a React component that adjusts the view layout to avoid
8+
* being covered by an open iOS UIPickerView modal. It's meant to be similar to
9+
* the built-in KeyboardAvoidingView component, but specifically tailored for
10+
* iOS picker modals.
11+
*
12+
* In order for this component to work correctly, all the pickers and the
13+
* PickerAvoidingView should have a PickerStateProvider ancestor.
14+
*
15+
* @param {React.ReactNode} props.children - The child components that should be
16+
* protected from obstruction by the picker modal
17+
*/
18+
export function PickerAvoidingView(props) {
19+
const context = React.useContext(PickerStateContext);
20+
const isPickerOpen = context && context.isPickerOpen;
21+
22+
return (
23+
<View style={StyleSheet.compose(props.style, {
24+
paddingBottom: isPickerOpen ? IOS_MODAL_HEIGHT : 0,
25+
})}>
26+
{props.children}
27+
</View>
28+
);
29+
}

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, { useState } 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] = 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: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@ 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
const preserveSpaces = (label) => {
1413
return label.replace(/ /g, '\u00a0');
1514
};
1615

1716
export default class RNPickerSelect extends PureComponent {
17+
static contextType = PickerStateContext;
18+
1819
static propTypes = {
1920
onValueChange: PropTypes.func.isRequired,
2021
items: PropTypes.arrayOf(
@@ -286,11 +287,16 @@ export default class RNPickerSelect extends PureComponent {
286287

287288
togglePicker(animate = false, postToggleCallback) {
288289
const { disabled } = this.props;
290+
const { showPicker } = this.state;
289291

290292
if (disabled) {
291293
return;
292294
}
293295

296+
if (this.context) {
297+
this.context.setIsPickerOpen(!showPicker);
298+
}
299+
294300
this.triggerOpenCloseCallbacks();
295301

296302
if (Keyboard.isVisible()) {
@@ -635,4 +641,4 @@ export default class RNPickerSelect extends PureComponent {
635641
}
636642
}
637643

638-
export { defaultStyles };
644+
export { defaultStyles, PickerStateProvider, PickerAvoidingView };

0 commit comments

Comments
 (0)