Skip to content

Commit 7a78f94

Browse files
committed
ios picker internal state inside Portal
1 parent 9ede62f commit 7a78f94

2 files changed

Lines changed: 177 additions & 44 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import * as React from "react";
2+
import { StyleSheet, Keyboard } from "react-native";
3+
import { SafeAreaView } from "react-native-safe-area-context";
4+
import { Picker as NativePickerComponent } from "@react-native-picker/picker";
5+
import Portal from "../Portal/Portal";
6+
import { Button } from "../Button";
7+
import { useDeepCompareMemo } from "../../utilities";
8+
import {
9+
CommonPickerProps,
10+
SinglePickerProps,
11+
normalizeToPickerOptions,
12+
PickerOption,
13+
} from "./PickerCommon";
14+
import PickerInputContainer from "./PickerInputContainer";
15+
import { withTheme } from "@draftbit/theme";
16+
import { IconSlot } from "../../interfaces/Icon";
17+
18+
/**
19+
* Duplicated version of NativePicker.tsx for maintaining state inside the Portal container to avoid this issue
20+
* https://github.com/react-native-picker/picker/issues/615
21+
*/
22+
23+
interface PortalPickerContentProps extends IconSlot {
24+
value: string | number | undefined;
25+
options: PickerOption[];
26+
placeholder?: string;
27+
onValueChange?: (value: string | number) => void;
28+
onClose: () => void;
29+
theme: any;
30+
autoDismissKeyboard?: boolean;
31+
}
32+
33+
const PortalPickerContent: React.FC<PortalPickerContentProps> = ({
34+
value,
35+
options,
36+
placeholder,
37+
onValueChange,
38+
onClose,
39+
Icon,
40+
theme,
41+
autoDismissKeyboard = true,
42+
}) => {
43+
const pickerRef = React.useRef<NativePickerComponent<string | number>>(null);
44+
45+
// Manage value state inside the Portal to avoid stale state issues across the Portal boundary
46+
const [internalValue, setInternalValue] = React.useState<
47+
string | number | undefined
48+
>(value);
49+
50+
React.useEffect(() => {
51+
setInternalValue(value);
52+
}, [value]);
53+
54+
React.useEffect(() => {
55+
if (autoDismissKeyboard) {
56+
Keyboard.dismiss();
57+
}
58+
}, [autoDismissKeyboard]);
59+
60+
return (
61+
<SafeAreaView style={styles.iosPickerContent}>
62+
<Button
63+
Icon={Icon}
64+
onPress={onClose}
65+
style={[styles.iosButton, { color: theme.colors.branding.primary }]}
66+
title="Close"
67+
/>
68+
<NativePickerComponent
69+
ref={pickerRef}
70+
testID="native-picker-component"
71+
selectedValue={internalValue}
72+
onValueChange={(newValue) => {
73+
setInternalValue(newValue);
74+
if (newValue !== placeholder) {
75+
onValueChange?.(newValue);
76+
} else if (newValue === placeholder) {
77+
onValueChange?.("");
78+
}
79+
}}
80+
style={styles.iosNativePicker}
81+
onBlur={onClose}
82+
>
83+
{options.map((option) => (
84+
<NativePickerComponent.Item
85+
testID="native-picker-item"
86+
label={option.label.toString()}
87+
value={option.value}
88+
key={option.value}
89+
/>
90+
))}
91+
</NativePickerComponent>
92+
</SafeAreaView>
93+
);
94+
};
95+
96+
const NativePicker: React.FC<CommonPickerProps & SinglePickerProps> = ({
97+
options: optionsProp = [],
98+
onValueChange,
99+
Icon,
100+
placeholder,
101+
value,
102+
autoDismissKeyboard = true,
103+
theme,
104+
disabled,
105+
...rest
106+
}) => {
107+
const [pickerVisible, setPickerVisible] = React.useState(false);
108+
109+
const options = useDeepCompareMemo(() => {
110+
const normalizedOptions = normalizeToPickerOptions(optionsProp);
111+
112+
// Underlying Picker component defaults selection to first element when value is not provided (or undefined)
113+
// Placholder must be the 1st option in order to allow selection of the 'actual' 1st option
114+
if (placeholder) {
115+
return [{ label: placeholder, value: placeholder }, ...normalizedOptions];
116+
} else {
117+
return normalizedOptions;
118+
}
119+
}, [placeholder, optionsProp]);
120+
121+
// When no placeholder is provided then first item should be marked selected to reflect underlying Picker internal state
122+
if (!placeholder && options.length && !value && value !== options[0].value) {
123+
onValueChange?.(options[0].value);
124+
}
125+
126+
return (
127+
<PickerInputContainer
128+
testID="native-picker"
129+
Icon={Icon}
130+
placeholder={placeholder}
131+
selectedValue={value}
132+
options={options}
133+
onPress={() => setPickerVisible(!pickerVisible)}
134+
disabled={disabled}
135+
{...rest}
136+
>
137+
{pickerVisible && !disabled && (
138+
<Portal>
139+
<PortalPickerContent
140+
value={value}
141+
options={options}
142+
placeholder={placeholder}
143+
onValueChange={onValueChange}
144+
onClose={() => setPickerVisible(false)}
145+
Icon={Icon}
146+
theme={theme}
147+
autoDismissKeyboard={autoDismissKeyboard}
148+
/>
149+
</Portal>
150+
)}
151+
</PickerInputContainer>
152+
);
153+
};
154+
155+
const styles = StyleSheet.create({
156+
iosNativePicker: {
157+
backgroundColor: "white",
158+
},
159+
iosPickerContent: {
160+
width: "100%",
161+
position: "absolute",
162+
bottom: 0,
163+
backgroundColor: "white",
164+
},
165+
iosButton: {
166+
backgroundColor: "transparent",
167+
borderWidth: 0,
168+
},
169+
});
170+
171+
export default withTheme(NativePicker);

packages/core/src/components/Picker/NativePicker.tsx

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import * as React from "react";
22
import { StyleSheet, Platform, Keyboard } from "react-native";
3-
import { SafeAreaView } from "react-native-safe-area-context";
43
import { Picker as NativePickerComponent } from "@react-native-picker/picker";
5-
import Portal from "../Portal/Portal";
6-
import { Button } from "../Button";
74
import { useDeepCompareMemo } from "../../utilities";
85
import {
96
CommonPickerProps,
@@ -13,7 +10,6 @@ import {
1310
import PickerInputContainer from "./PickerInputContainer";
1411
import { withTheme } from "@draftbit/theme";
1512

16-
const isIos = Platform.OS === "ios";
1713
const isAndroid = Platform.OS === "android";
1814
const isWeb = Platform.OS === "web";
1915

@@ -61,7 +57,7 @@ const NativePicker: React.FC<CommonPickerProps & SinglePickerProps> = ({
6157
onValueChange?.("");
6258
}
6359
}}
64-
style={isIos ? styles.iosNativePicker : styles.nativePicker}
60+
style={styles.nativePicker}
6561
onBlur={() => setPickerVisible(false)}
6662
>
6763
{options.map((option) => (
@@ -75,29 +71,6 @@ const NativePicker: React.FC<CommonPickerProps & SinglePickerProps> = ({
7571
</NativePickerComponent>
7672
);
7773

78-
const renderPicker = () => {
79-
if (isIos) {
80-
return (
81-
<Portal>
82-
<SafeAreaView style={styles.iosPickerContent}>
83-
<Button
84-
Icon={Icon}
85-
onPress={() => setPickerVisible(!pickerVisible)}
86-
style={[
87-
styles.iosButton,
88-
{ color: theme.colors.branding.primary },
89-
]}
90-
title="Close"
91-
/>
92-
{renderNativePicker()}
93-
</SafeAreaView>
94-
</Portal>
95-
);
96-
} else {
97-
return renderNativePicker();
98-
}
99-
};
100-
10174
React.useEffect(() => {
10275
if (pickerVisible && pickerRef.current) {
10376
pickerRef?.current?.focus();
@@ -123,7 +96,9 @@ const NativePicker: React.FC<CommonPickerProps & SinglePickerProps> = ({
12396
>
12497
{/* Web version is collapsed by default, always show to allow direct expand */}
12598
{/* Android version needs to always be visible to allow .focus() call to launch the dialog */}
126-
{(pickerVisible || isAndroid || isWeb) && !disabled && renderPicker()}
99+
{(pickerVisible || isAndroid || isWeb) &&
100+
!disabled &&
101+
renderNativePicker()}
127102
</PickerInputContainer>
128103
);
129104
};
@@ -141,26 +116,13 @@ const styles = StyleSheet.create({
141116
opacity: 0,
142117
...Platform.select({
143118
web: {
144-
height: "100%", //To have the <select/> element fill the height
119+
height: "100%",
145120
},
146121
android: {
147-
opacity: 0, // picker is a dialog, we don't want to show the default 'picker button' component
122+
opacity: 0,
148123
},
149124
}),
150125
},
151-
iosNativePicker: {
152-
backgroundColor: "white",
153-
},
154-
iosPickerContent: {
155-
width: "100%",
156-
position: "absolute",
157-
bottom: 0,
158-
backgroundColor: "white",
159-
},
160-
iosButton: {
161-
backgroundColor: "transparent",
162-
borderWidth: 0,
163-
},
164126
});
165127

166128
export default withTheme(NativePicker);

0 commit comments

Comments
 (0)