@react-native-picker/picker
Version:
React Native Picker for iOS, Android, macOS, and Windows
231 lines (207 loc) • 6.43 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.
*
*
* This is a controlled component version of RNCPickerIOS
*
* @format
* @flow
*/
;
import * as React from 'react';
import {processColor, StyleSheet, View} from 'react-native';
import RNCPickerNativeComponent, {
Commands as iOSPickerCommands,
} from './RNCPickerNativeComponent';
import type {SyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes';
import type {
ColorValue,
ViewStyleProp,
} from 'react-native/Libraries/StyleSheet/StyleSheet';
import type {Element, ChildrenArray} from 'react';
type PickerIOSChangeEvent = SyntheticEvent<
$ReadOnly<{|
newValue: number | string,
newIndex: number,
|}>,
>;
type RNCPickerIOSItemType = $ReadOnly<{|
label: ?Label,
value: ?(number | string),
textColor: ?ColorValue,
testID: ?string,
|}>;
type Label = Stringish | number;
type Props = $ReadOnly<{|
...ViewProps,
// $FlowFixMe
children: ChildrenArray<Element<typeof PickerIOSItem>>,
itemStyle?: ?ViewStyleProp,
numberOfLines: ?number,
onChange?: ?(event: PickerIOSChangeEvent) => mixed,
onValueChange?: ?(itemValue: string | number, itemIndex: number) => mixed,
selectedValue: ?(number | string),
selectionColor: ?string,
themeVariant: ?string,
|}>;
type ItemProps = $ReadOnly<{|
label: ?Label,
value?: ?(number | string),
color?: ?ColorValue,
testID?: ?string,
|}>;
type CallbackRef<T> = (T) => mixed;
type ObjectRef<T> = {current: T, ...};
type Ref<T> = CallbackRef<T> | ObjectRef<T>;
/**
* Constructs a new ref that forwards new values to each of the given refs. The
* given refs will always be invoked in the order that they are supplied.
*
* WARNING: A known problem of merging refs using this approach is that if any
* of the given refs change, the returned callback ref will also be changed. If
* the returned callback ref is supplied as a `ref` to a React element, this may
* lead to problems with the given refs being invoked more times than desired.
*/
function useMergeRefs<T>(...refs: $ReadOnlyArray<?Ref<T>>): CallbackRef<T> {
return React.useCallback(
(current: T) => {
for (const ref of refs) {
if (ref != null) {
if (typeof ref === 'function') {
ref(current);
} else {
ref.current = current;
}
}
}
},
[...refs], // eslint-disable-line react-hooks/exhaustive-deps
);
}
// $FlowFixMe
const PickerIOSItem: RNCPickerIOSItemType = (props: ItemProps): null => {
return null;
};
const PickerIOSWithForwardedRef: React.AbstractComponent<
Props,
React.ElementRef<typeof RNCPickerNativeComponent>,
> = React.forwardRef(function PickerIOS(props, forwardedRef): React.Node {
const {
children,
selectedValue,
selectionColor,
themeVariant,
testID,
itemStyle,
numberOfLines,
onChange,
onValueChange,
style,
accessibilityLabel,
accessibilityHint,
} = props;
const nativePickerRef = React.useRef<React.ElementRef<
typeof RNCPickerNativeComponent,
> | null>(null);
// $FlowFixMe
const ref = useMergeRefs(nativePickerRef, forwardedRef);
const [nativeSelectedIndex, setNativeSelectedIndex] = React.useState({
value: null,
});
const [items, selectedIndex] = React.useMemo(() => {
// eslint-disable-next-line no-shadow
let selectedIndex = 0;
// eslint-disable-next-line no-shadow
const items = React.Children.toArray<$FlowFixMe>(children).map(
(child, index) => {
if (child === null) {
return null;
}
if (child.props.value === selectedValue) {
selectedIndex = index;
}
return {
value: child.props.value,
label: child.props.label,
textColor: processColor(child.props.color),
testID: child.props.testID,
};
},
);
return [items, selectedIndex];
}, [children, selectedValue]);
let parsedNumberOfLines = Math.round(numberOfLines ?? 1);
if (parsedNumberOfLines < 1) {
parsedNumberOfLines = 1;
}
React.useLayoutEffect(() => {
let jsValue = 0;
React.Children.toArray<$FlowFixMe>(children).forEach(function (
child: $FlowFixMe,
index: number,
) {
if (child.props.value === selectedValue) {
jsValue = index;
}
});
// 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.
const shouldUpdateNativePicker =
nativeSelectedIndex.value != null &&
nativeSelectedIndex.value !== jsValue;
if (shouldUpdateNativePicker && nativePickerRef.current) {
if (global?.nativeFabricUIManager) {
iOSPickerCommands.setNativeSelectedIndex(
nativePickerRef.current,
jsValue,
);
} else {
nativePickerRef.current.setNativeProps({
selectedIndex: jsValue,
});
}
}
}, [selectedValue, nativeSelectedIndex, children]);
const _onChange = React.useCallback(
(event: $FlowFixMe) => {
onChange?.(event);
onValueChange?.(event.nativeEvent.newValue, event.nativeEvent.newIndex);
setNativeSelectedIndex({value: event.nativeEvent.newIndex});
},
[onChange, onValueChange],
);
return (
<View style={style}>
<RNCPickerNativeComponent
ref={ref}
themeVariant={themeVariant}
testID={testID}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
style={[styles.pickerIOS, itemStyle]}
// $FlowFixMe
items={items}
onChange={_onChange}
numberOfLines={parsedNumberOfLines}
selectedIndex={selectedIndex}
selectionColor={processColor(selectionColor)}
/>
</View>
);
});
const styles = StyleSheet.create({
pickerIOS: {
// The picker will conform to whatever width is given, but we do
// have to set the component's height explicitly on the
// surrounding view to ensure it gets rendered.
height: 216,
},
});
// $FlowFixMe
PickerIOSWithForwardedRef.Item = PickerIOSItem;
export default PickerIOSWithForwardedRef;