@react-native-picker/picker
Version:
React Native Picker for iOS, Android, macOS, and Windows
241 lines (217 loc) • 6.95 kB
JavaScript
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
;
import * as React from 'react';
import {
findNodeHandle,
NativeSyntheticEvent,
processColor,
UIManager,
} from 'react-native';
import AndroidDialogPickerNativeComponent, {
Commands as AndroidDialogPickerCommands,
} from './AndroidDialogPickerNativeComponent';
import AndroidDropdownPickerNativeComponent, {
Commands as AndroidDropdownPickerCommands,
} from './AndroidDropdownPickerNativeComponent';
import type {TextStyleProp} from 'StyleSheet';
const MODE_DROPDOWN = 'dropdown';
type PickerAndroidProps = $ReadOnly<{|
children?: React.Node,
style?: ?TextStyleProp,
selectedValue?: ?(number | string),
enabled?: ?boolean,
mode?: ?('dialog' | 'dropdown'),
onBlur?: (e: NativeSyntheticEvent<undefined>) => mixed,
onFocus?: (e: NativeSyntheticEvent<undefined>) => mixed,
onValueChange?: ?(itemValue: ?(string | number), itemIndex: number) => mixed,
prompt?: ?string,
testID?: string,
dropdownIconColor?: string,
numberOfLines?: ?number,
|}>;
type PickerRef = React.ElementRef<
| typeof AndroidDialogPickerNativeComponent
| typeof AndroidDropdownPickerNativeComponent,
>;
/**
* Not exposed as a public API - use <Picker> instead.
*/
function PickerAndroid(props: PickerAndroidProps, ref: PickerRef): React.Node {
const pickerRef = React.useRef(null);
const FABRIC_ENABLED = !!global?.nativeFabricUIManager;
const [nativeSelectedIndex, setNativeSelectedIndex] = React.useState({
value: null,
});
React.useImperativeHandle(ref, () => {
const viewManagerConfig = UIManager.getViewManagerConfig(
props.mode === MODE_DROPDOWN
? 'RNCAndroidDialogPicker'
: 'RNCAndroidDropdownPicker',
);
return {
blur: () => {
if (!viewManagerConfig.Commands) {
return;
}
if (FABRIC_ENABLED) {
if (props.mode === MODE_DROPDOWN) {
AndroidDropdownPickerCommands.blur(pickerRef.current);
} else {
AndroidDialogPickerCommands.blur(pickerRef.current);
}
} else {
UIManager.dispatchViewManagerCommand(
findNodeHandle(pickerRef.current),
viewManagerConfig.Commands.blur,
[],
);
}
},
focus: () => {
if (!viewManagerConfig.Commands) {
return;
}
if (FABRIC_ENABLED) {
if (props.mode === MODE_DROPDOWN) {
AndroidDropdownPickerCommands.focus(pickerRef.current);
} else {
AndroidDialogPickerCommands.focus(pickerRef.current);
}
} else {
UIManager.dispatchViewManagerCommand(
findNodeHandle(pickerRef.current),
viewManagerConfig.Commands.focus,
[],
);
}
},
};
});
React.useLayoutEffect(() => {
let jsValue = 0;
React.Children.toArray(props.children).map((child, index) => {
if (child === null) {
return null;
}
if (child?.props?.value === props.selectedValue) {
jsValue = index;
}
});
const shouldUpdateNativePicker =
nativeSelectedIndex.value != null &&
nativeSelectedIndex.value !== jsValue;
// This is necessary in case native updates the switch and JS decides
// that the update should be ignored and we should stick with the value
// that we have in JS.
if (shouldUpdateNativePicker && pickerRef.current) {
if (FABRIC_ENABLED) {
if (props.mode === MODE_DROPDOWN) {
AndroidDropdownPickerCommands.setNativeSelected(
pickerRef.current,
selected,
);
} else {
AndroidDialogPickerCommands.setNativeSelected(
pickerRef.current,
selected,
);
}
} else {
pickerRef.current.setNativeProps({
selected,
});
}
}
}, [
props.selectedValue,
nativeSelectedIndex,
props.children,
FABRIC_ENABLED,
props.mode,
selected,
]);
const [items, selected] = React.useMemo(() => {
// eslint-disable-next-line no-shadow
let selected = 0;
// eslint-disable-next-line no-shadow
const items = React.Children.toArray(props.children).map((child, index) => {
if (child === null) {
return null;
}
if (child?.props?.value === props.selectedValue) {
selected = index;
}
const {enabled = true} = child.props || {};
const {color, contentDescription, label, style = {}} = child.props || {};
const processedColor = processColor(color);
return {
color: color == null ? null : processedColor,
contentDescription,
label: String(label),
enabled,
style: {
...style,
// there seems to be a problem with codegen, where it would assign to an item
// the last defined value of the font size if not set explicitly
// 0 is handled on the native side as "not set"
fontSize: style.fontSize ?? 0,
color: style.color ? processColor(style.color) : null,
backgroundColor: style.backgroundColor
? processColor(style.backgroundColor)
: null,
},
};
});
return [items, selected];
}, [props.children, props.selectedValue]);
const onSelect = React.useCallback(
({nativeEvent}: PickerItemSelectSyntheticEvent) => {
const {position} = nativeEvent;
const onValueChange = props.onValueChange;
if (onValueChange != null) {
if (position >= 0) {
const children = React.Children.toArray(props.children).filter(
(item) => item != null,
);
const value = children[position]?.props?.value;
if (value !== undefined) {
onValueChange(value, position);
}
} else {
onValueChange(null, position);
}
}
setNativeSelectedIndex({value: position});
},
[props.children, props.onValueChange],
);
const Picker =
props.mode === MODE_DROPDOWN
? AndroidDropdownPickerNativeComponent
: AndroidDialogPickerNativeComponent;
const rootProps = {
accessibilityLabel: props.accessibilityLabel,
enabled: props.enabled,
items,
onBlur: props.onBlur,
onFocus: props.onFocus,
onSelect,
prompt: props.prompt,
selected,
style: props.style,
dropdownIconColor: processColor(props.dropdownIconColor),
dropdownIconRippleColor: processColor(props.dropdownIconRippleColor),
testID: props.testID,
numberOfLines: props.numberOfLines,
};
return <Picker ref={pickerRef} {...rootProps} />;
}
export default React.forwardRef<PickerAndroidProps>(PickerAndroid);