@monchilin/react-native-dropdown
Version:
Dropdown component for React Native
632 lines (563 loc) • 17.3 kB
JavaScript
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { ActivityIndicator, Animated, Dimensions, Easing, FlatList, StyleSheet, Text, TouchableHighlight, TouchableNativeFeedback, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
import Modal from './Modal';
const truth = () => true;
const isNumber = o => typeof o === 'number';
function id(v) {
return v;
}
const TOUCHABLE_ELEMENTS = ['TouchableHighlight', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback']; // TODO react native for web not support useWindowDimensions()
const {
width: windowWidth,
height: windowHeight
} = Dimensions.get('window');
const useEffectWithSkipFirst = (callback, deps) => {
const isFirstRun = useRef(true);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
return callback(); // eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
};
const useAnimation = ({
visible,
transitionHide,
transitionShow,
meta
}) => {
const [state, setState] = useState(false);
const anim = useRef(new Animated.Value(90)).current;
const [style, setStyle] = useState({});
useEffectWithSkipFirst(() => {
if (visible) {
const transitionShowConfig = transitions[transitionShow];
const interpolate = anim.interpolate(transitionShowConfig.interpolate(meta));
anim.setValue(transitionShowConfig.initialValue);
setState(true);
switch (transitionShow) {
case 'flipUp':
setStyle({
transform: [{
rotateX: interpolate
}]
});
break;
case 'scaleIn':
setStyle({
transform: [{
scaleX: interpolate,
scaleY: 1
}]
});
break;
case 'fadeIn':
setStyle({
opacity: interpolate
});
break;
case 'slideUp':
setStyle({
height: interpolate
});
break;
default:
setStyle({});
} // @ts-ignore
Animated[transitionShowConfig.animationType](anim, transitionShowConfig.config).start();
} else {
const transitionHideConfig = transitions[transitionHide];
const interpolate = anim.interpolate(transitionHideConfig.interpolate(meta));
anim.setValue(transitionHideConfig.initialValue);
switch (transitionHide) {
case 'flipDown':
setStyle({
transform: [{
rotateX: interpolate
}]
});
break;
case 'scaleOut':
setStyle({
transform: [{
scaleX: interpolate,
scaleY: 1
}]
});
break;
case 'fadeOut':
setStyle({
opacity: interpolate
});
break;
case 'slideDown':
setStyle({
height: interpolate
});
break;
default:
setStyle({});
} // @ts-ignore
Animated[transitionHideConfig.animationType](anim, transitionHideConfig.config).start(() => {
setState(false);
});
}
}, [visible]);
return {
visible: state,
style
};
};
const transitions = {
flipUp: {
config: {
toValue: 0,
friction: 10,
tension: 20,
useNativeDriver: true
},
interpolate: () => ({
inputRange: [0, 180],
outputRange: ['0deg', '180deg']
}),
initialValue: 90,
animationType: 'spring'
},
flipDown: {
config: {
toValue: 90,
friction: 10,
tension: 40,
useNativeDriver: true
},
interpolate: () => ({
inputRange: [0, 90],
outputRange: ['0deg', '90deg']
}),
initialValue: 0,
animationType: 'spring'
},
scaleIn: {
config: {
toValue: 1,
friction: 10,
tension: 10,
useNativeDriver: true
},
interpolate: () => ({
inputRange: [0, 1],
outputRange: [0, 1]
}),
initialValue: 0,
animationType: 'spring'
},
scaleOut: {
config: {
toValue: 0,
friction: 10,
tension: 10,
useNativeDriver: true
},
interpolate: () => ({
inputRange: [0, 1],
outputRange: [0, 1]
}),
initialValue: 1,
animationType: 'spring'
},
fadeIn: {
config: {
toValue: 1,
duration: 500,
useNativeDriver: true
},
interpolate: () => ({
inputRange: [0, 1],
outputRange: [0, 1]
}),
initialValue: 0,
animationType: 'timing'
},
fadeOut: {
config: {
toValue: 0,
duration: 500,
useNativeDriver: true
},
interpolate: () => ({
inputRange: [0, 1],
outputRange: [0, 1]
}),
initialValue: 1,
animationType: 'timing'
},
slideUp: {
config: {
toValue: 100,
useNativeDriver: false
},
interpolate: meta => ({
inputRange: [0, 100],
delay: 800,
easing: Easing.in,
outputRange: [0, meta.dropdownHeight]
}),
initialValue: 0,
animationType: 'timing'
},
slideDown: {
config: {
toValue: 0,
useNativeDriver: false
},
interpolate: meta => ({
inputRange: [0, 100],
delay: 800,
easing: Easing.sin,
outputRange: [0, meta.dropdownHeight]
}),
initialValue: 100,
animationType: 'timing'
}
};
const usePosition = ({
heightSourceStyle,
widthSourceStyle
}) => {
const height = useMemo(() => {
const style = StyleSheet.flatten(heightSourceStyle.find(item => StyleSheet.flatten(item).height));
const _height = (style === null || style === void 0 ? void 0 : style.height) ? style.height.toString() : '-1';
return Number.parseFloat(_height);
}, [heightSourceStyle]);
const width = useMemo(() => {
const style = StyleSheet.flatten(widthSourceStyle.find(item => StyleSheet.flatten(item).width));
const _width = (style === null || style === void 0 ? void 0 : style.width) ? style.width.toString() : '-1';
return Number.parseFloat(_width);
}, [widthSourceStyle]);
return {
height,
width
};
};
const SplitLine = () => {
return /*#__PURE__*/React.createElement(View, {
style: styles.splitLint
});
};
function Component({
defaultIndex,
defaultLabel = 'Please select',
index,
onSelect = truth,
dataSource,
disabled = false,
loading = false,
animated = true,
transitionShow = 'flipUp',
transitionHide = 'flipDown',
scrollEnabled = true,
keyExtractor = (_, itemIndex) => itemIndex.toString(),
adjustFrame = id,
renderItem,
renderSeparator = SplitLine,
showSeparator = true,
renderLabel,
onDropdownWillShow = truth,
onDropdownWillHide = truth,
rootContainerStyle = {},
rootContainerProps = {},
labelContainerStyle = {},
labelContainerDisabledStyle = {},
labelContainerProps = {},
labelStyle = {},
labelDisabledStyle = {},
labelProps = {},
modalProps = {},
dropdownStyle = {},
dropdownProps = {},
itemTouchableProps = {},
itemLabelStyle = {},
itemLabelProps = {},
itemLabelHighlightStyle = {},
children
}, ref) {
const _button = useRef(null);
const _buttonFrame = useRef({
x: 0,
y: 0,
w: 0,
h: 0
});
const [dropdownVisible, setDropdownVisible] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(() => {
return isNumber(index) ? index : isNumber(defaultIndex) ? defaultIndex : -1;
});
const dropDownSize = usePosition({
heightSourceStyle: [rootContainerStyle, dropdownStyle, styles.dropdown],
widthSourceStyle: [rootContainerStyle, dropdownStyle, rootContainerStyle]
});
const {
style: dropdownAnimatedStyle,
visible: _dropdownVisibleForAnimation
} = useAnimation({
visible: dropdownVisible,
transitionHide,
transitionShow,
meta: {
dropdownHeight: dropDownSize.height,
dropdownWidth: dropDownSize.width
}
});
const labelText = useMemo(() => {
return renderLabel ? renderLabel(dataSource[selectedIndex], selectedIndex) : dataSource[selectedIndex] ? dataSource[selectedIndex] : defaultLabel;
}, [dataSource, defaultLabel, renderLabel, selectedIndex]);
useEffectWithSkipFirst(() => {
if (isNumber(index)) {
setSelectedIndex(index);
} else {
setSelectedIndex(-1);
}
}, [index]);
const hide = useCallback(() => {
setDropdownVisible(false);
}, []);
const show = useCallback(() => {
setDropdownVisible(true);
}, []);
const select = useCallback(newIndex => {
setSelectedIndex(newIndex);
onSelect(newIndex);
}, [onSelect]);
useImperativeHandle(ref, () => ({
select,
hide,
show
}));
const onLayout = () => {
var _button$current;
if (!((_button$current = _button.current) === null || _button$current === void 0 ? void 0 : _button$current.measure)) {
return;
}
_button.current.measure((_fx, _fy, width, height, px, py) => {
_buttonFrame.current = {
x: px,
y: py,
w: width,
h: height
};
});
};
const _onRequestClose = () => {
if (onDropdownWillHide(dataSource[selectedIndex], selectedIndex) !== false) {
hide();
}
};
const _onModalPress = () => {
if (onDropdownWillHide(dataSource[selectedIndex], selectedIndex) !== false) {
hide();
}
};
const _onLabelPress = () => {
if (onDropdownWillShow(dataSource[selectedIndex], selectedIndex) !== false) {
show();
}
};
const _onPressModalItem = (newIndex, item) => {
// 除非是 false 否则更新 index
if (onSelect(newIndex, item) !== false) {
setSelectedIndex(newIndex);
}
if (onDropdownWillHide(dataSource[newIndex], newIndex) !== false) {
setDropdownVisible(false);
}
};
const getDropdownHorizontalBorderWidthFromStyle = useCallback(() => {
var _style$borderWidth, _style$borderLeftWidt, _style$borderRightWid;
const style = StyleSheet.flatten([dropdownStyle, rootContainerStyle, styles.dropdown]);
const borderWidth = (_style$borderWidth = style.borderWidth) !== null && _style$borderWidth !== void 0 ? _style$borderWidth : 0;
const borderLeftWidth = (_style$borderLeftWidt = style.borderLeftWidth) !== null && _style$borderLeftWidt !== void 0 ? _style$borderLeftWidt : 0;
const borderRightWidth = (_style$borderRightWid = style.borderRightWidth) !== null && _style$borderRightWid !== void 0 ? _style$borderRightWid : 0;
if (!borderLeftWidth && !borderRightWidth) {
return borderWidth * 2;
}
return borderLeftWidth + borderRightWidth;
}, [rootContainerStyle, dropdownStyle]);
const _calcPosition = () => {
// 首先根据 style 的对象获取 dropdown 容器的高度
const dropdownHeight = dropDownSize.height; // x: 按钮的 x 点(相对于屏幕左上角)
// y: 按钮的 y 点(相对于屏幕顶点)
// w: 按钮的 width
// h: 按钮的 height
const {
x,
y,
w,
h
} = _buttonFrame.current; // 距离底部的空间
const buttonSpace = windowHeight - y - h; // 距离右边的空间
const rightSpace = windowWidth - x; // 如果距离底部的空间大于等于 dropdown 的高度 或者 底部空间
const showInBottom = buttonSpace >= dropdownHeight || buttonSpace >= y;
const showInLeft = rightSpace >= x;
const positionStyle = {
height: dropdownHeight,
top: showInBottom ? y + h : Math.max(0, y - dropdownHeight)
};
if (showInLeft) {
positionStyle.left = x;
} else {
const dropdownWidth = dropDownSize.width;
if (dropdownWidth !== -1) {
positionStyle.width = dropdownWidth;
}
positionStyle.right = rightSpace - w;
}
return adjustFrame(positionStyle);
};
const _renderLoading = () => {
return /*#__PURE__*/React.createElement(ActivityIndicator, {
size: "small"
});
};
const _renderLabel = () => {
return /*#__PURE__*/React.createElement(TouchableOpacity, _extends({
ref: _button,
disabled: disabled,
onPress: _onLabelPress,
style: [labelContainerStyle, disabled && styles.labelContainerDisabled, disabled && labelContainerDisabledStyle]
}, labelContainerProps), children !== null && children !== void 0 ? children : /*#__PURE__*/React.createElement(Text, _extends({
style: [styles.label, labelStyle, disabled && styles.labelDisabled, disabled && labelDisabledStyle],
numberOfLines: 1
}, labelProps), labelText));
};
const _renderItem = ({
index: newIndex,
item
}) => {
const highlighted = newIndex === selectedIndex;
const row = renderItem ? renderItem(item, newIndex, highlighted) : /*#__PURE__*/React.createElement(Text, _extends({
style: [styles.dropdownTextStyle, itemLabelStyle, highlighted && styles.highlightedRowText, highlighted && itemLabelHighlightStyle]
}, itemLabelProps), item);
const preservedProps = {
onPress: () => _onPressModalItem(newIndex, item),
...itemTouchableProps
};
if (TOUCHABLE_ELEMENTS.find(name => name === row.type.displayName)) {
const props = { ...row.props,
onPress: preservedProps.onPress
};
const {
children: realChildren
} = row.props;
switch (row.type.displayName) {
case 'TouchableHighlight':
{
return /*#__PURE__*/React.createElement(TouchableHighlight, props, realChildren);
}
case 'TouchableOpacity':
{
return /*#__PURE__*/React.createElement(TouchableOpacity, props, realChildren);
}
case 'TouchableWithoutFeedback':
{
return /*#__PURE__*/React.createElement(TouchableWithoutFeedback, props, realChildren);
}
// TODO react native web not support TouchableNativeFeedback
case 'TouchableNativeFeedback':
{
console.warn('react native web not support TouchableNativeFeedback');
return /*#__PURE__*/React.createElement(TouchableNativeFeedback, props, row);
}
default:
break;
}
}
return /*#__PURE__*/React.createElement(TouchableHighlight, preservedProps, row);
};
const _renderDropdown = () => {
const dropdownWidth = dropDownSize.width !== -1 ? dropDownSize.width - getDropdownHorizontalBorderWidthFromStyle() : undefined;
return /*#__PURE__*/React.createElement(FlatList, _extends({
scrollEnabled: scrollEnabled,
style: [dropdownStyle, {
width: dropdownWidth,
height: dropDownSize.height
}],
data: dataSource,
renderItem: _renderItem,
keyExtractor: keyExtractor,
ItemSeparatorComponent: showSeparator ? renderSeparator : null,
automaticallyAdjustContentInsets: false,
showsVerticalScrollIndicator: false
}, dropdownProps));
};
const _renderModel = () => {
const frameStyle = _calcPosition();
return /*#__PURE__*/React.createElement(Modal, _extends({
animated: animated,
animationType: 'none',
visible: _dropdownVisibleForAnimation,
transparent: true,
onRequestClose: _onRequestClose,
supportedOrientations: ['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']
}, modalProps), /*#__PURE__*/React.createElement(TouchableWithoutFeedback, {
disabled: !dropdownVisible,
onPress: _onModalPress
}, /*#__PURE__*/React.createElement(View, {
style: styles.modal
}, /*#__PURE__*/React.createElement(Animated.View, {
style: [styles.dropdown, frameStyle, animated && dropdownAnimatedStyle]
}, loading ? _renderLoading() : _renderDropdown()))));
};
return /*#__PURE__*/React.createElement(View, _extends({
onLayout: onLayout,
style: rootContainerStyle
}, rootContainerProps), _renderLabel(), _renderModel());
}
export default /*#__PURE__*/React.forwardRef(Component);
const styles = StyleSheet.create({
label: {
fontSize: 12
},
modal: {
flexGrow: 1
},
dropdown: {
height: (33 + StyleSheet.hairlineWidth) * 4,
position: 'absolute',
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'lightgray',
borderRadius: 2,
backgroundColor: '#ffffff',
justifyContent: 'center'
},
loading: {
alignSelf: 'center'
},
dropdownTextStyle: {
paddingHorizontal: 6,
paddingVertical: 10,
fontSize: 11,
color: 'rgba(0,0,0,.65)',
backgroundColor: 'white',
textAlignVertical: 'center'
},
highlightedRowText: {
color: 'black'
},
highlightedRow: {
backgroundColor: '#f5f5f5'
},
labelDisabled: {
color: 'rgba(0,0,0,.25)'
},
labelContainerDisabled: {
backgroundColor: '#fff'
},
splitLint: {
backgroundColor: 'rgb(217, 217, 217)',
height: 1,
width: '100%'
}
});
//# sourceMappingURL=Dropdown.js.map