@valdio/react-native-scrollable-tabview
Advanced tools
| import React from 'react' | ||
| import ReactNative from 'react-native' | ||
| const {TouchableNativeFeedback} = ReactNative | ||
| export default Button = (props) => | ||
| <TouchableNativeFeedback | ||
| delayPressIn={0} | ||
| background={TouchableNativeFeedback.SelectableBackground()} // eslint-disable-line new-cap | ||
| {...props} | ||
| > | ||
| {props.children} | ||
| </TouchableNativeFeedback> | ||
| import React from 'react' | ||
| import ReactNative from 'react-native' | ||
| const {TouchableOpacity} = ReactNative | ||
| export default Button = (props) => { | ||
| return <TouchableOpacity {...props}> | ||
| {props.children} | ||
| </TouchableOpacity> | ||
| } | ||
| import React from 'react' | ||
| import ReactNative from 'react-native' | ||
| const {TouchableOpacity} = ReactNative | ||
| export default Button = (props) => { | ||
| return <TouchableOpacity {...props}> | ||
| {props.children} | ||
| </TouchableOpacity> | ||
| } | ||
| import React, {Component} from 'react' | ||
| import ReactNative from 'react-native' | ||
| import PropTypes from 'prop-types' | ||
| const { | ||
| StyleSheet, | ||
| Text, | ||
| View, | ||
| ViewPropTypes, | ||
| Animated | ||
| } = ReactNative | ||
| import Button from './Button' | ||
| import ScrollableTabView from './index' | ||
| const defaultProps = { | ||
| activeTextColor: 'navy', | ||
| inactiveTextColor: 'black', | ||
| backgroundColor: null | ||
| } | ||
| export default class DefaultTabBar extends React.Component { | ||
| constructor(props) { | ||
| super(props) | ||
| this.renderTab = this.renderTab.bind(this) | ||
| } | ||
| renderTabOption(name, page) { | ||
| } | ||
| renderTab(name, page, isTabActive, onPressHandler) { | ||
| const {activeTextColor, inactiveTextColor, textStyle} = this.props | ||
| const textColor = isTabActive ? activeTextColor : inactiveTextColor | ||
| const fontWeight = isTabActive ? 'bold' : 'normal' | ||
| return <Button | ||
| style={styles.flexOne} | ||
| key={name} | ||
| accessible={true} | ||
| accessibilityLabel={name} | ||
| accessibilityTraits='button' | ||
| onPress={() => onPressHandler(page)} | ||
| > | ||
| <View style={[styles.tab, this.props.tabStyle]}> | ||
| <Text style={[{color: textColor, fontWeight}, textStyle]}> | ||
| {name} | ||
| </Text> | ||
| </View> | ||
| </Button> | ||
| } | ||
| render() { | ||
| const containerWidth = this.props.containerWidth | ||
| const numberOfTabs = this.props.tabs.length | ||
| const tabUnderlineStyle = { | ||
| position: 'absolute', | ||
| width: containerWidth / numberOfTabs, | ||
| height: 4, | ||
| backgroundColor: 'navy', | ||
| bottom: 0 | ||
| } | ||
| const left = this.props.scrollValue.interpolate({ | ||
| inputRange: [0, 1], outputRange: [0, containerWidth / numberOfTabs] | ||
| }) | ||
| return ( | ||
| <View style={[styles.tabs, {backgroundColor: this.props.backgroundColor}, this.props.style]}> | ||
| {this.props.tabs.map((name, page) => { | ||
| const isTabActive = this.props.activeTab === page | ||
| const renderTab = this.props.renderTab || this.renderTab | ||
| return renderTab(name, page, isTabActive, this.props.goToPage) // () => | ||
| })} | ||
| <Animated.View style={[tabUnderlineStyle, {left}, this.props.underlineStyle]}/> | ||
| </View> | ||
| ) | ||
| } | ||
| } | ||
| DefaultTabBar.propTypes = { | ||
| goToPage: PropTypes.func, | ||
| activeTab: PropTypes.number, | ||
| tabs: PropTypes.array, | ||
| backgroundColor: PropTypes.string, | ||
| activeTextColor: PropTypes.string, | ||
| inactiveTextColor: PropTypes.string, | ||
| textStyle: Text.propTypes.style, | ||
| tabStyle: ViewPropTypes.style, | ||
| renderTab: PropTypes.func, | ||
| underlineStyle: ViewPropTypes.style | ||
| } | ||
| DefaultTabBar.defaultProps = { | ||
| activeTextColor: 'navy', | ||
| inactiveTextColor: 'black', | ||
| backgroundColor: null | ||
| } | ||
| const styles = StyleSheet.create({ | ||
| tab: { | ||
| flex: 1, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| paddingBottom: 10 | ||
| }, | ||
| flexOne: { | ||
| flex: 1 | ||
| }, | ||
| tabs: { | ||
| height: 50, | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-around', | ||
| borderWidth: 1, | ||
| borderTopWidth: 0, | ||
| borderLeftWidth: 0, | ||
| borderRightWidth: 0, | ||
| borderColor: '#ccc' | ||
| } | ||
| }) | ||
+390
| import React, {Component} from 'react' | ||
| import ReactNative from 'react-native' | ||
| import PropTypes from 'prop-types' | ||
| const { | ||
| ViewPropTypes, | ||
| Dimensions, | ||
| View, | ||
| Animated, | ||
| ScrollView, | ||
| StyleSheet, | ||
| InteractionManager, | ||
| Platform, | ||
| RefreshControl | ||
| } = ReactNative | ||
| // import TimerMixin from 'react-timer-mixin' | ||
| import SceneComponent from './SceneComponent' | ||
| import DefaultTabBar from './DefaultTabBar' | ||
| import ScrollableTabBar from './ScrollableTabBar' | ||
| /** | ||
| * pullToRefresh: function used to trigger pull to refresh action. | ||
| * Function muns have a callback response in order to stop pull to refresh action. | ||
| * refreshControlStyle: style of RefreshControl | ||
| */ | ||
| class ScrollableTabView extends Component { | ||
| constructor(props) { | ||
| super(props) | ||
| this.state = this.getInitialState() | ||
| this._handleLayout = this._handleLayout.bind(this) | ||
| this.goToPage = this.goToPage.bind(this) | ||
| this.renderTabBar = this.renderTabBar.bind(this) | ||
| this.updateSceneKeys = this.updateSceneKeys.bind(this) | ||
| this.newSceneKeys = this.newSceneKeys.bind(this) | ||
| this._shouldRenderSceneKey = this._shouldRenderSceneKey.bind(this) | ||
| this._keyExists = this._keyExists.bind(this) | ||
| this._makeSceneKey = this._makeSceneKey.bind(this) | ||
| this.renderScrollableContent = this.renderScrollableContent.bind(this) | ||
| this._composeScenes = this._composeScenes.bind(this) | ||
| this._onMomentumScrollBeginAndEnd = this._onMomentumScrollBeginAndEnd.bind(this) | ||
| this._updateSelectedPage = this._updateSelectedPage.bind(this) | ||
| this.renderCollapsableBar = this.renderCollapsableBar.bind(this) | ||
| } | ||
| // mixins = [TimerMixin] | ||
| /* getDefaultProps() { | ||
| return { | ||
| tabBarPosition: 'top', | ||
| initialPage: 0, | ||
| page: -1, | ||
| onChangeTab: () => { | ||
| }, | ||
| onScroll: () => { | ||
| }, | ||
| contentProps: {}, | ||
| scrollWithoutAnimation: false, | ||
| locked: false, | ||
| prerenderingSiblingsNumber: 0 | ||
| } | ||
| }*/ | ||
| getInitialState() { | ||
| const width = Dimensions.get('window').width | ||
| return { | ||
| currentPage: this.props.initialPage, | ||
| scrollX: new Animated.Value(this.props.initialPage * width), | ||
| scrollValue: new Animated.Value(this.props.initialPage), | ||
| containerWidth: width, | ||
| sceneKeys: this.newSceneKeys({currentPage: this.props.initialPage}), | ||
| refreshing: false | ||
| } | ||
| } | ||
| _onRefresh = () => { | ||
| //if there is not pullToRefresh function do nothing | ||
| if (!this.props.pullToRefresh) | ||
| return | ||
| this.setState({refreshing: true}) | ||
| this.props.pullToRefresh(response => { | ||
| this.setState({refreshing: false}) | ||
| }) | ||
| } | ||
| componentDidMount() { | ||
| setTimeout(() => { | ||
| InteractionManager.runAfterInteractions(() => { | ||
| if (Platform.OS === 'android') { | ||
| this.goToPage(this.props.initialPage, false) | ||
| } | ||
| }) | ||
| }, 0) | ||
| this.state.scrollX.addListener(({value}) => { | ||
| const scrollValue = value / this.state.containerWidth | ||
| this.state.scrollValue.setValue(scrollValue) | ||
| this.props.onScroll(scrollValue) | ||
| }) | ||
| } | ||
| componentWillReceiveProps(props) { | ||
| if (props.children !== this.props.children) { | ||
| this.updateSceneKeys({page: this.state.currentPage, children: props.children}) | ||
| } | ||
| if (props.page >= 0 && props.page !== this.state.currentPage) { | ||
| this.goToPage(props.page) | ||
| } | ||
| } | ||
| goToPage(pageNumber, animated = !this.props.scrollWithoutAnimation) { | ||
| const offset = pageNumber * this.state.containerWidth | ||
| if (this.scrollView && this.scrollView._component && this.scrollView._component.scrollTo) { | ||
| this.scrollView._component.scrollTo({x: offset, y: 0, animated}) | ||
| } | ||
| const currentPage = this.state.currentPage | ||
| this.updateSceneKeys({ | ||
| page: pageNumber, | ||
| callback: this._onChangeTab.bind(this, currentPage, pageNumber) | ||
| }) | ||
| } | ||
| renderTabBar(props) { | ||
| if (this.props.renderTabBar === false) { | ||
| return null | ||
| } else if (this.props.renderTabBar) { | ||
| return React.cloneElement(this.props.renderTabBar(props), props) | ||
| } else { | ||
| return <DefaultTabBar {...props} /> | ||
| } | ||
| } | ||
| updateSceneKeys({ | ||
| page, children = this.props.children, callback = () => { | ||
| } | ||
| }) { | ||
| let newKeys = this.newSceneKeys({previousKeys: this.state.sceneKeys, currentPage: page, children}) | ||
| this.setState({currentPage: page, sceneKeys: newKeys}, callback) | ||
| } | ||
| newSceneKeys({previousKeys = [], currentPage = 0, children = this.props.children}) { | ||
| let newKeys = [] | ||
| this._children(children).forEach((child, idx) => { | ||
| let key = this._makeSceneKey(child, idx) | ||
| if (this._keyExists(previousKeys, key) || | ||
| this._shouldRenderSceneKey(idx, currentPage)) { | ||
| newKeys.push(key) | ||
| } | ||
| }) | ||
| return newKeys | ||
| } | ||
| _shouldRenderSceneKey(idx, currentPageKey) { | ||
| let numOfSibling = this.props.prerenderingSiblingsNumber | ||
| return (idx < (currentPageKey + numOfSibling + 1) && | ||
| idx > (currentPageKey - numOfSibling - 1)) | ||
| } | ||
| _keyExists(sceneKeys, key) { | ||
| return sceneKeys.find((sceneKey) => key === sceneKey) | ||
| } | ||
| _makeSceneKey(child, idx) { | ||
| return child.props.tabLabel + '_' + idx | ||
| } | ||
| renderScrollableContent() { | ||
| //in case of the collapsible scroll view the pull to refresh animation will be applied on the container | ||
| //on the other case the refresh animations will be applied here. | ||
| const isContainerScrollView = !!this.props.collapsableBar | ||
| const scenes = this._composeScenes() | ||
| return <Animated.ScrollView | ||
| refreshControl={!isContainerScrollView && this.props.pullToRefresh && typeof this.props.pullToRefresh === 'function' && | ||
| <RefreshControl style={this.props.refreshControlStyle || {}} | ||
| refreshing={this.state.refreshing} | ||
| onRefresh={this._onRefresh}/> || undefined} | ||
| showsVerticalScrollIndicator={this.props.showsVerticalScrollIndicator} | ||
| showsHorizontalScrollIndicator={this.props.showsHorizontalScrollIndicator} | ||
| horizontal | ||
| pagingEnabled | ||
| automaticallyAdjustContentInsets={false} | ||
| contentOffset={{x: this.props.initialPage * this.state.containerWidth}} | ||
| ref={scrollView => this.scrollView = scrollView} | ||
| onScroll={Animated.event([{nativeEvent: {contentOffset: {x: this.state.scrollX}}}], {useNativeDriver: true})} | ||
| onMomentumScrollBegin={this._onMomentumScrollBeginAndEnd} | ||
| onMomentumScrollEnd={this._onMomentumScrollBeginAndEnd} | ||
| scrollEventThrottle={16} | ||
| scrollsToTop={false} | ||
| scrollEnabled={!this.props.locked} | ||
| directionalLockEnabled | ||
| alwaysBounceVertical={false} | ||
| keyboardDismissMode="on-drag" | ||
| {...this.props.contentProps} | ||
| > | ||
| {scenes} | ||
| </Animated.ScrollView> | ||
| } | ||
| _composeScenes() { | ||
| return this._children().map((child, idx) => { | ||
| let key = this._makeSceneKey(child, idx) | ||
| let element | ||
| if (!!this.props.collapsableBar) { | ||
| element = this.state.currentPage === idx ? child : null | ||
| } else { | ||
| element = this._keyExists(this.state.sceneKeys, key) ? child : <View tabLabel={child.props.tabLabel}/> | ||
| } | ||
| return <SceneComponent | ||
| key={child.key} | ||
| shouldUpdated={!!this.props.collapsableBar || this._shouldRenderSceneKey(idx, this.state.currentPage)} | ||
| style={{width: this.state.containerWidth}} | ||
| > | ||
| {element} | ||
| </SceneComponent> | ||
| }) | ||
| } | ||
| _onMomentumScrollBeginAndEnd(e) { | ||
| const offsetX = e.nativeEvent.contentOffset.x | ||
| const page = Math.round(offsetX / this.state.containerWidth) | ||
| if (this.state.currentPage !== page) { | ||
| this._updateSelectedPage(page) | ||
| } | ||
| } | ||
| _updateSelectedPage(nextPage) { | ||
| let localNextPage = nextPage | ||
| if (typeof localNextPage === 'object') { | ||
| localNextPage = nextPage.nativeEvent.position | ||
| } | ||
| const currentPage = this.state.currentPage | ||
| this.updateSceneKeys({ | ||
| page: localNextPage, | ||
| callback: this._onChangeTab.bind(this, currentPage, localNextPage) | ||
| }) | ||
| } | ||
| _onChangeTab(prevPage, currentPage) { | ||
| this.props.onChangeTab({ | ||
| i: currentPage, | ||
| ref: this._children()[currentPage], | ||
| from: prevPage | ||
| }) | ||
| if (this.contentScrollDistance >= this.collapsableBarHeight) { | ||
| this.contentView.scrollTo({x: 0, y: this.collapsableBarHeight, animated: false}) | ||
| } | ||
| } | ||
| _handleLayout(e) { | ||
| const {width} = e.nativeEvent.layout | ||
| if (Math.round(width) !== Math.round(this.state.containerWidth)) { | ||
| this.setState({containerWidth: width}) | ||
| this.requestAnimationFrame(() => { | ||
| this.goToPage(this.state.currentPage) | ||
| }) | ||
| } | ||
| } | ||
| _children(children = this.props.children) { | ||
| return React.Children.map(children, (child) => child) | ||
| } | ||
| renderCollapsableBar() { | ||
| if (!this.props.collapsableBar) { | ||
| return null | ||
| } | ||
| return React.cloneElement(this.props.collapsableBar, { | ||
| onLayout: event => { | ||
| this.collapsableBarHeight = event.nativeEvent.layout.height | ||
| } | ||
| }) | ||
| } | ||
| render() { | ||
| let overlayTabs = (this.props.tabBarPosition === 'overlayTop' || this.props.tabBarPosition === 'overlayBottom') | ||
| let tabBarProps = { | ||
| goToPage: this.goToPage, | ||
| tabs: this._children().map((child) => child.props.tabLabel), | ||
| activeTab: this.state.currentPage, | ||
| scrollX: this.state.scrollX, | ||
| scrollValue: this.state.scrollValue, | ||
| containerWidth: this.state.containerWidth | ||
| } | ||
| if (this.props.tabBarBackgroundColor) { | ||
| tabBarProps.backgroundColor = this.props.tabBarBackgroundColor | ||
| } | ||
| if (this.props.tabBarActiveTextColor) { | ||
| tabBarProps.activeTextColor = this.props.tabBarActiveTextColor | ||
| } | ||
| if (this.props.tabBarInactiveTextColor) { | ||
| tabBarProps.inactiveTextColor = this.props.tabBarInactiveTextColor | ||
| } | ||
| if (this.props.tabBarTextStyle) { | ||
| tabBarProps.textStyle = this.props.tabBarTextStyle | ||
| } | ||
| if (this.props.tabBarUnderlineStyle) { | ||
| tabBarProps.underlineStyle = this.props.tabBarUnderlineStyle | ||
| } | ||
| if (overlayTabs) { | ||
| tabBarProps.style = { | ||
| position: 'absolute', | ||
| left: 0, | ||
| right: 0, | ||
| [this.props.tabBarPosition === 'overlayTop' ? 'top' : 'bottom']: 0 | ||
| } | ||
| } | ||
| const ContainerView = this.props.collapsableBar ? ScrollView : View | ||
| const isScrollView = this.props.collapsableBar ? true : false | ||
| return (<ContainerView | ||
| refreshControl={isScrollView && this.props.pullToRefresh && typeof this.props.pullToRefresh === 'function' && | ||
| <RefreshControl style={this.props.refreshControlStyle || {}} | ||
| refreshing={this.state.refreshing} | ||
| onRefresh={this._onRefresh}/> || undefined} | ||
| showsVerticalScrollIndicator={isScrollView && this.props.showsVerticalScrollIndicator} | ||
| showsHorizontalScrollIndicator={isScrollView && this.props.showsHorizontalScrollIndicator} | ||
| style={[styles.container, this.props.style]} | ||
| onLayout={this._handleLayout} //()=> | ||
| ref={contentView => this.contentView = contentView} | ||
| onMomentumScrollEnd={event => { | ||
| this.contentScrollDistance = event.nativeEvent.contentOffset.y | ||
| }} | ||
| stickyHeaderIndices={this.props.collapsableBar ? [1] : []}> | ||
| <View style={[{flex: 1}, this.props.contentStyle || {}]}> | ||
| {this.renderCollapsableBar()} | ||
| {this.props.tabBarPosition === 'top' && this.renderTabBar(tabBarProps)} | ||
| {this.renderScrollableContent()} | ||
| {(this.props.tabBarPosition === 'bottom' || overlayTabs) && this.renderTabBar(tabBarProps)} | ||
| </View> | ||
| </ContainerView> | ||
| ) | ||
| } | ||
| } | ||
| ScrollableTabView.defaultProps = { | ||
| tabBarPosition: 'top', | ||
| initialPage: 0, | ||
| page: -1, | ||
| onChangeTab: () => { | ||
| }, | ||
| onScroll: () => { | ||
| }, | ||
| contentProps: {}, | ||
| scrollWithoutAnimation: false, | ||
| locked: false, | ||
| prerenderingSiblingsNumber: 0 | ||
| } | ||
| ScrollableTabView.propTypes = { | ||
| tabBarPosition: PropTypes.oneOf(['top', 'bottom', 'overlayTop', 'overlayBottom']), | ||
| initialPage: PropTypes.number, | ||
| page: PropTypes.number, | ||
| onChangeTab: PropTypes.func, | ||
| onScroll: PropTypes.func, | ||
| renderTabBar: PropTypes.any, | ||
| style: ViewPropTypes.style, | ||
| contentProps: PropTypes.object, | ||
| scrollWithoutAnimation: PropTypes.bool, | ||
| locked: PropTypes.bool, | ||
| prerenderingSiblingsNumber: PropTypes.number, | ||
| collapsableBar: PropTypes.node | ||
| } | ||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1 | ||
| }, | ||
| scrollableContentAndroid: { | ||
| flex: 1 | ||
| } | ||
| }) | ||
| module.exports = { | ||
| DefaultTabBar, | ||
| ScrollableTabBar, | ||
| ScrollableTabView | ||
| } |
| import React from 'react' | ||
| import ReactNative from 'react-native' | ||
| const {View, StyleSheet} = ReactNative | ||
| import StaticContainer from './StaticContainer' | ||
| export default SceneComponent = (Props) => { | ||
| const {shouldUpdated, ...props} = Props | ||
| return <View {...props}> | ||
| <StaticContainer shouldUpdate={shouldUpdated}> | ||
| {props.children} | ||
| </StaticContainer> | ||
| </View> | ||
| } |
| import React, {Component} from 'react' | ||
| import ReactNative from 'react-native' | ||
| import PropTypes from 'prop-types' | ||
| const { | ||
| ViewPropTypes, | ||
| Dimensions, | ||
| View, | ||
| Animated, | ||
| ScrollView, | ||
| StyleSheet, | ||
| InteractionManager, | ||
| Platform, | ||
| Text, | ||
| I18nManager | ||
| } = ReactNative | ||
| import Button from './Button' | ||
| const WINDOW_WIDTH = Dimensions.get('window').width | ||
| export default class ScrollableTabBar extends Component { | ||
| constructor(props) { | ||
| super(props) | ||
| this.state = this.getInitialState() | ||
| this.updateView = this.updateView.bind(this) | ||
| this.necessarilyMeasurementsCompleted = this.necessarilyMeasurementsCompleted.bind(this) | ||
| this.updateTabPanel = this.updateTabPanel.bind(this) | ||
| this.updateTabUnderline = this.updateTabUnderline.bind(this) | ||
| this.updateTabUnderline = this.updateTabUnderline.bind(this) | ||
| this.renderTab = this.renderTab.bind(this) | ||
| this.measureTab = this.measureTab.bind(this) | ||
| this.onTabContainerLayout = this.onTabContainerLayout.bind(this) | ||
| this.onContainerLayout = this.onContainerLayout.bind(this) | ||
| } | ||
| getInitialState() { | ||
| this._tabsMeasurements = [] | ||
| return { | ||
| _leftTabUnderline: new Animated.Value(0), | ||
| _widthTabUnderline: new Animated.Value(0), | ||
| _containerWidth: null | ||
| } | ||
| } | ||
| componentDidMount() { | ||
| this.props.scrollValue.addListener(this.updateView) | ||
| } | ||
| updateView(offset) { | ||
| const position = Math.floor(offset.value) | ||
| const pageOffset = offset.value % 1 | ||
| const tabCount = this.props.tabs.length | ||
| const lastTabPosition = tabCount - 1 | ||
| if (tabCount === 0 || offset.value < 0 || offset.value > lastTabPosition) { | ||
| return | ||
| } | ||
| if (this.necessarilyMeasurementsCompleted(position, position === lastTabPosition)) { | ||
| this.updateTabPanel(position, pageOffset) | ||
| this.updateTabUnderline(position, pageOffset, tabCount) | ||
| } | ||
| } | ||
| necessarilyMeasurementsCompleted(position, isLastTab) { | ||
| return this._tabsMeasurements[position] && | ||
| (isLastTab || this._tabsMeasurements[position + 1]) && | ||
| this._tabContainerMeasurements && | ||
| this._containerMeasurements | ||
| } | ||
| updateTabPanel(position, pageOffset) { | ||
| const containerWidth = this._containerMeasurements.width | ||
| const tabWidth = this._tabsMeasurements[position].width | ||
| const nextTabMeasurements = this._tabsMeasurements[position + 1] | ||
| const nextTabWidth = nextTabMeasurements && nextTabMeasurements.width || 0 | ||
| const tabOffset = this._tabsMeasurements[position].left | ||
| const absolutePageOffset = pageOffset * tabWidth | ||
| let newScrollX = tabOffset + absolutePageOffset | ||
| // center tab and smooth tab change (for when tabWidth changes a lot between two tabs) | ||
| newScrollX -= (containerWidth - (1 - pageOffset) * tabWidth - pageOffset * nextTabWidth) / 2 | ||
| newScrollX = newScrollX >= 0 ? newScrollX : 0 | ||
| if (Platform.OS === 'android') { | ||
| this._scrollView && this._scrollView.scrollTo({x: newScrollX, y: 0, animated: false}) | ||
| } else { | ||
| const rightBoundScroll = this._tabContainerMeasurements.width - (this._containerMeasurements.width) | ||
| newScrollX = newScrollX > rightBoundScroll ? rightBoundScroll : newScrollX | ||
| this._scrollView && this._scrollView.scrollTo({x: newScrollX, y: 0, animated: false}) | ||
| } | ||
| } | ||
| updateTabUnderline(position, pageOffset, tabCount) { | ||
| const lineLeft = this._tabsMeasurements[position].left | ||
| const lineRight = this._tabsMeasurements[position].right | ||
| if (position < tabCount - 1) { | ||
| const nextTabLeft = this._tabsMeasurements[position + 1].left | ||
| const nextTabRight = this._tabsMeasurements[position + 1].right | ||
| const newLineLeft = (pageOffset * nextTabLeft + (1 - pageOffset) * lineLeft) | ||
| const newLineRight = (pageOffset * nextTabRight + (1 - pageOffset) * lineRight) | ||
| this.state._leftTabUnderline.setValue(newLineLeft) | ||
| this.state._widthTabUnderline.setValue(newLineRight - newLineLeft) | ||
| } else { | ||
| this.state._leftTabUnderline.setValue(lineLeft) | ||
| this.state._widthTabUnderline.setValue(lineRight - lineLeft) | ||
| } | ||
| } | ||
| renderTab(name, page, isTabActive, onPressHandler, onLayoutHandler) { | ||
| const {activeTextColor, inactiveTextColor, textStyle} = this.props | ||
| const textColor = isTabActive ? activeTextColor : inactiveTextColor | ||
| const fontWeight = isTabActive ? 'bold' : 'normal' | ||
| return <Button | ||
| key={`${name}_${page}`} | ||
| accessible={true} | ||
| accessibilityLabel={name} | ||
| accessibilityTraits='button' | ||
| onPress={() => onPressHandler(page)} | ||
| onLayout={onLayoutHandler} | ||
| > | ||
| <View style={[styles.tab, this.props.tabStyle]}> | ||
| <Text style={[{color: textColor, fontWeight}, textStyle]}> | ||
| {name} | ||
| </Text> | ||
| </View> | ||
| </Button> | ||
| } | ||
| measureTab(page, event) { | ||
| const {x, width, height} = event.nativeEvent.layout | ||
| this._tabsMeasurements[page] = {left: x, right: x + width, width, height} | ||
| this.updateView({value: this.props.scrollValue._value}) | ||
| } | ||
| render() { | ||
| const tabUnderlineStyle = { | ||
| position: 'absolute', | ||
| height: 4, | ||
| backgroundColor: 'navy', | ||
| bottom: 0 | ||
| } | ||
| const key = I18nManager.isRTL ? 'right' : 'left' | ||
| const dynamicTabUnderline = { | ||
| [`${key}`]: this.state._leftTabUnderline, | ||
| width: this.state._widthTabUnderline | ||
| } | ||
| return <View | ||
| style={[styles.container, {backgroundColor: this.props.backgroundColor}, this.props.style]} | ||
| onLayout={this.onContainerLayout} | ||
| > | ||
| <ScrollView | ||
| automaticallyAdjustContentInsets={false} | ||
| ref={(scrollView) => { | ||
| this._scrollView = scrollView | ||
| }} | ||
| horizontal={true} | ||
| showsHorizontalScrollIndicator={false} | ||
| showsVerticalScrollIndicator={false} | ||
| directionalLockEnabled={true} | ||
| onScroll={this.props.onScroll} | ||
| bounces={false} | ||
| scrollsToTop={false} | ||
| > | ||
| <View | ||
| style={[styles.tabs, {width: this.state._containerWidth}, this.props.tabsContainerStyle]} | ||
| ref={'tabContainer'} | ||
| onLayout={this.onTabContainerLayout} | ||
| > | ||
| {this.props.tabs.map((name, page) => { | ||
| const isTabActive = this.props.activeTab === page | ||
| const renderTab = this.props.renderTab || this.renderTab | ||
| return renderTab(name, page, isTabActive, this.props.goToPage, this.measureTab.bind(this, page)) | ||
| })} | ||
| <Animated.View style={[tabUnderlineStyle, dynamicTabUnderline, this.props.underlineStyle]}/> | ||
| </View> | ||
| </ScrollView> | ||
| </View> | ||
| } | ||
| componentWillReceiveProps(nextProps) { | ||
| // If the tabs change, force the width of the tabs container to be recalculated | ||
| if (JSON.stringify(this.props.tabs) !== JSON.stringify(nextProps.tabs) && this.state._containerWidth) { | ||
| this.setState({_containerWidth: null}) | ||
| } | ||
| } | ||
| onTabContainerLayout(e) { | ||
| this._tabContainerMeasurements = e.nativeEvent.layout | ||
| let width = this._tabContainerMeasurements.width | ||
| if (width < WINDOW_WIDTH) { | ||
| width = WINDOW_WIDTH | ||
| } | ||
| this.setState({_containerWidth: width}) | ||
| this.updateView({value: this.props.scrollValue._value}) | ||
| } | ||
| onContainerLayout(e) { | ||
| this._containerMeasurements = e.nativeEvent.layout | ||
| this.updateView({value: this.props.scrollValue._value}) | ||
| } | ||
| } | ||
| ScrollableTabBar.defaultProps = { | ||
| scrollOffset: 52, | ||
| activeTextColor: 'navy', | ||
| inactiveTextColor: 'black', | ||
| backgroundColor: null, | ||
| style: {}, | ||
| tabStyle: {}, | ||
| tabsContainerStyle: {}, | ||
| underlineStyle: {} | ||
| } | ||
| ScrollableTabBar.propTypes = { | ||
| goToPage: PropTypes.func, | ||
| activeTab: PropTypes.number, | ||
| tabs: PropTypes.array, | ||
| backgroundColor: PropTypes.string, | ||
| activeTextColor: PropTypes.string, | ||
| inactiveTextColor: PropTypes.string, | ||
| scrollOffset: PropTypes.number, | ||
| style: ViewPropTypes.style, | ||
| tabStyle: ViewPropTypes.style, | ||
| tabsContainerStyle: ViewPropTypes.style, | ||
| textStyle: Text.propTypes.style, | ||
| renderTab: PropTypes.func, | ||
| underlineStyle: ViewPropTypes.style, | ||
| onScroll: PropTypes.func | ||
| } | ||
| const styles = StyleSheet.create({ | ||
| tab: { | ||
| height: 49, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| paddingLeft: 20, | ||
| paddingRight: 20 | ||
| }, | ||
| container: { | ||
| height: 50, | ||
| borderWidth: 1, | ||
| borderTopWidth: 0, | ||
| borderLeftWidth: 0, | ||
| borderRightWidth: 0, | ||
| borderColor: '#ccc' | ||
| }, | ||
| tabs: { | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-around' | ||
| } | ||
| }) |
| import React, {Component} from 'react' | ||
| export default class StaticContainer extends Component { | ||
| shouldComponentUpdate(nextProps: Object): boolean { | ||
| return !!nextProps.shouldUpdate | ||
| } | ||
| render(): ?ReactElement { | ||
| const child = this.props.children | ||
| if (child === null || child === false) { | ||
| return null | ||
| } | ||
| return React.Children.only(child) | ||
| } | ||
| } | ||
+4
-6
| { | ||
| "name": "@valdio/react-native-scrollable-tabview", | ||
| "version": "0.8.9", | ||
| "version": "0.8.10", | ||
| "description": "", | ||
| "main": "index.js", | ||
| "main": "lib/index.js", | ||
| "scripts": { | ||
@@ -22,3 +22,3 @@ "lint": "eslint -c .eslintrc . --ignore-path .gitignore", | ||
| ], | ||
| "author": "Valdio Veliu", | ||
| "author": "Valdio Veliu <Valdio Veliu>", | ||
| "license": "MIT", | ||
@@ -30,6 +30,4 @@ "bugs": { | ||
| "homepage": "https://github.com/valdio/react-native-scrollable-tabview#readme", | ||
| "dependencies": { | ||
| "react-timer-mixin": "^0.13.3" | ||
| }, | ||
| "dependencies": {}, | ||
| "devDependencies": {} | ||
| } |
+3
-2
@@ -113,3 +113,4 @@ | ||
| - **`tabBarTextStyle`** _(Object)_ - Additional styles to the tab bar's text. Example: `{fontFamily: 'Roboto', fontSize: 15}` | ||
| - **`style`** _([View.propTypes.style](https://facebook.github.io/react-native/docs/view.html#style))_ | ||
| - **`style`** _([View.propTypes.style](https://facebook.github.io/react-native/docs/view.html#style))_ - Container style | ||
| - **`contentStyle`** _([View.propTypes.style](https://facebook.github.io/react-native/docs/view.html#style))_ - Content style | ||
| - **`contentProps`** _(Object)_ - props that are applied to root `ScrollView`/`ViewPagerAndroid`. Note that overriding defaults set by the library may break functionality; see the source for details. | ||
@@ -133,3 +134,3 @@ - **`scrollWithoutAnimation`** _(Bool)_ - on tab press change tab without animation. | ||
| _onRefresh = (callback) => { | ||
| networkReqyest().then(response => callback(response)) | ||
| networkRequest().then(response => callback(response)) | ||
| } | ||
@@ -136,0 +137,0 @@ |
| import React from 'react' | ||
| import ReactNative from 'react-native' | ||
| const { | ||
| TouchableNativeFeedback, | ||
| View | ||
| } = ReactNative | ||
| export default Button = (props) => | ||
| <TouchableNativeFeedback | ||
| delayPressIn={0} | ||
| background={TouchableNativeFeedback.SelectableBackground()} // eslint-disable-line new-cap | ||
| {...props} | ||
| > | ||
| {props.children} | ||
| </TouchableNativeFeedback> | ||
| import React from 'react' | ||
| import ReactNative from 'react-native' | ||
| const { | ||
| TouchableOpacity, | ||
| View | ||
| } = ReactNative | ||
| export default Button = (props) => { | ||
| return <TouchableOpacity {...props}> | ||
| {props.children} | ||
| </TouchableOpacity> | ||
| } | ||
| import React from 'react' | ||
| import ReactNative from 'react-native' | ||
| const { | ||
| TouchableOpacity, | ||
| View | ||
| } = ReactNative | ||
| export default Button = (props) => { | ||
| return <TouchableOpacity {...props}> | ||
| {props.children} | ||
| </TouchableOpacity> | ||
| } | ||
-118
| import React, {Component} from 'react' | ||
| import ReactNative from 'react-native' | ||
| import PropTypes from 'prop-types' | ||
| const { | ||
| StyleSheet, | ||
| Text, | ||
| View, | ||
| ViewPropTypes, | ||
| Animated | ||
| } = ReactNative | ||
| import Button from './Button' | ||
| import ScrollableTabView from './index' | ||
| const defaultProps = { | ||
| activeTextColor: 'navy', | ||
| inactiveTextColor: 'black', | ||
| backgroundColor: null | ||
| } | ||
| export default class DefaultTabBar extends React.Component { | ||
| constructor(props) { | ||
| super(props) | ||
| this.renderTab = this.renderTab.bind(this) | ||
| } | ||
| renderTabOption(name, page) { | ||
| } | ||
| renderTab(name, page, isTabActive, onPressHandler) { | ||
| const {activeTextColor, inactiveTextColor, textStyle} = this.props | ||
| const textColor = isTabActive ? activeTextColor : inactiveTextColor | ||
| const fontWeight = isTabActive ? 'bold' : 'normal' | ||
| return <Button | ||
| style={styles.flexOne} | ||
| key={name} | ||
| accessible={true} | ||
| accessibilityLabel={name} | ||
| accessibilityTraits='button' | ||
| onPress={() => onPressHandler(page)} | ||
| > | ||
| <View style={[styles.tab, this.props.tabStyle]}> | ||
| <Text style={[{color: textColor, fontWeight}, textStyle]}> | ||
| {name} | ||
| </Text> | ||
| </View> | ||
| </Button> | ||
| } | ||
| render() { | ||
| const containerWidth = this.props.containerWidth | ||
| const numberOfTabs = this.props.tabs.length | ||
| const tabUnderlineStyle = { | ||
| position: 'absolute', | ||
| width: containerWidth / numberOfTabs, | ||
| height: 4, | ||
| backgroundColor: 'navy', | ||
| bottom: 0 | ||
| } | ||
| const left = this.props.scrollValue.interpolate({ | ||
| inputRange: [0, 1], outputRange: [0, containerWidth / numberOfTabs] | ||
| }) | ||
| return ( | ||
| <View style={[styles.tabs, {backgroundColor: this.props.backgroundColor}, this.props.style]}> | ||
| {this.props.tabs.map((name, page) => { | ||
| const isTabActive = this.props.activeTab === page | ||
| const renderTab = this.props.renderTab || this.renderTab | ||
| return renderTab(name, page, isTabActive, this.props.goToPage) // () => | ||
| })} | ||
| <Animated.View style={[tabUnderlineStyle, {left}, this.props.underlineStyle]}/> | ||
| </View> | ||
| ) | ||
| } | ||
| } | ||
| DefaultTabBar.propTypes = { | ||
| goToPage: PropTypes.func, | ||
| activeTab: PropTypes.number, | ||
| tabs: PropTypes.array, | ||
| backgroundColor: PropTypes.string, | ||
| activeTextColor: PropTypes.string, | ||
| inactiveTextColor: PropTypes.string, | ||
| textStyle: Text.propTypes.style, | ||
| tabStyle: ViewPropTypes.style, | ||
| renderTab: PropTypes.func, | ||
| underlineStyle: ViewPropTypes.style | ||
| } | ||
| DefaultTabBar.defaultProps = { | ||
| activeTextColor: 'navy', | ||
| inactiveTextColor: 'black', | ||
| backgroundColor: null | ||
| } | ||
| const styles = StyleSheet.create({ | ||
| tab: { | ||
| flex: 1, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| paddingBottom: 10 | ||
| }, | ||
| flexOne: { | ||
| flex: 1 | ||
| }, | ||
| tabs: { | ||
| height: 50, | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-around', | ||
| borderWidth: 1, | ||
| borderTopWidth: 0, | ||
| borderLeftWidth: 0, | ||
| borderRightWidth: 0, | ||
| borderColor: '#ccc' | ||
| } | ||
| }) | ||
-388
| import React, {Component} from 'react' | ||
| import ReactNative from 'react-native' | ||
| import PropTypes from 'prop-types' | ||
| const { | ||
| ViewPropTypes, | ||
| Dimensions, | ||
| View, | ||
| Animated, | ||
| ScrollView, | ||
| StyleSheet, | ||
| InteractionManager, | ||
| Platform, | ||
| RefreshControl | ||
| } = ReactNative | ||
| import TimerMixin from 'react-timer-mixin' | ||
| import SceneComponent from './SceneComponent' | ||
| import DefaultTabBar from './DefaultTabBar' | ||
| import ScrollableTabBar from './ScrollableTabBar' | ||
| /** | ||
| * pullToRefresh: function used to trigger pull to refresh action. | ||
| * Function muns have a callback response in order to stop pull to refresh action. | ||
| * refreshControlStyle: style of RefreshControl | ||
| */ | ||
| class ScrollableTabView extends Component { | ||
| constructor(props) { | ||
| super(props) | ||
| this.state = this.getInitialState() | ||
| this._handleLayout = this._handleLayout.bind(this) | ||
| this.goToPage = this.goToPage.bind(this) | ||
| this.renderTabBar = this.renderTabBar.bind(this) | ||
| this.updateSceneKeys = this.updateSceneKeys.bind(this) | ||
| this.newSceneKeys = this.newSceneKeys.bind(this) | ||
| this._shouldRenderSceneKey = this._shouldRenderSceneKey.bind(this) | ||
| this._keyExists = this._keyExists.bind(this) | ||
| this._makeSceneKey = this._makeSceneKey.bind(this) | ||
| this.renderScrollableContent = this.renderScrollableContent.bind(this) | ||
| this._composeScenes = this._composeScenes.bind(this) | ||
| this._onMomentumScrollBeginAndEnd = this._onMomentumScrollBeginAndEnd.bind(this) | ||
| this._updateSelectedPage = this._updateSelectedPage.bind(this) | ||
| this.renderCollapsableBar = this.renderCollapsableBar.bind(this) | ||
| } | ||
| mixins = [TimerMixin] | ||
| /* getDefaultProps() { | ||
| return { | ||
| tabBarPosition: 'top', | ||
| initialPage: 0, | ||
| page: -1, | ||
| onChangeTab: () => { | ||
| }, | ||
| onScroll: () => { | ||
| }, | ||
| contentProps: {}, | ||
| scrollWithoutAnimation: false, | ||
| locked: false, | ||
| prerenderingSiblingsNumber: 0 | ||
| } | ||
| }*/ | ||
| getInitialState() { | ||
| const width = Dimensions.get('window').width | ||
| return { | ||
| currentPage: this.props.initialPage, | ||
| scrollX: new Animated.Value(this.props.initialPage * width), | ||
| scrollValue: new Animated.Value(this.props.initialPage), | ||
| containerWidth: width, | ||
| sceneKeys: this.newSceneKeys({currentPage: this.props.initialPage}), | ||
| refreshing: false | ||
| } | ||
| } | ||
| _onRefresh = () => { | ||
| //if there is not pullToRefresh function do nothing | ||
| if (!this.props.pullToRefresh) | ||
| return | ||
| this.setState({refreshing: true}) | ||
| this.props.pullToRefresh(response => { | ||
| this.setState({refreshing: false}) | ||
| }) | ||
| } | ||
| componentDidMount() { | ||
| setTimeout(() => { | ||
| InteractionManager.runAfterInteractions(() => { | ||
| if (Platform.OS === 'android') { | ||
| this.goToPage(this.props.initialPage, false) | ||
| } | ||
| }) | ||
| }, 0) | ||
| this.state.scrollX.addListener(({value}) => { | ||
| const scrollValue = value / this.state.containerWidth | ||
| this.state.scrollValue.setValue(scrollValue) | ||
| this.props.onScroll(scrollValue) | ||
| }) | ||
| } | ||
| componentWillReceiveProps(props) { | ||
| if (props.children !== this.props.children) { | ||
| this.updateSceneKeys({page: this.state.currentPage, children: props.children}) | ||
| } | ||
| if (props.page >= 0 && props.page !== this.state.currentPage) { | ||
| this.goToPage(props.page) | ||
| } | ||
| } | ||
| goToPage(pageNumber, animated = !this.props.scrollWithoutAnimation) { | ||
| const offset = pageNumber * this.state.containerWidth | ||
| if (this.scrollView && this.scrollView._component && this.scrollView._component.scrollTo) { | ||
| this.scrollView._component.scrollTo({x: offset, y: 0, animated}) | ||
| } | ||
| const currentPage = this.state.currentPage | ||
| this.updateSceneKeys({ | ||
| page: pageNumber, | ||
| callback: this._onChangeTab.bind(this, currentPage, pageNumber) | ||
| }) | ||
| } | ||
| renderTabBar(props) { | ||
| if (this.props.renderTabBar === false) { | ||
| return null | ||
| } else if (this.props.renderTabBar) { | ||
| return React.cloneElement(this.props.renderTabBar(props), props) | ||
| } else { | ||
| return <DefaultTabBar {...props} /> | ||
| } | ||
| } | ||
| updateSceneKeys({ | ||
| page, children = this.props.children, callback = () => { | ||
| } | ||
| }) { | ||
| let newKeys = this.newSceneKeys({previousKeys: this.state.sceneKeys, currentPage: page, children}) | ||
| this.setState({currentPage: page, sceneKeys: newKeys}, callback) | ||
| } | ||
| newSceneKeys({previousKeys = [], currentPage = 0, children = this.props.children}) { | ||
| let newKeys = [] | ||
| this._children(children).forEach((child, idx) => { | ||
| let key = this._makeSceneKey(child, idx) | ||
| if (this._keyExists(previousKeys, key) || | ||
| this._shouldRenderSceneKey(idx, currentPage)) { | ||
| newKeys.push(key) | ||
| } | ||
| }) | ||
| return newKeys | ||
| } | ||
| _shouldRenderSceneKey(idx, currentPageKey) { | ||
| let numOfSibling = this.props.prerenderingSiblingsNumber | ||
| return (idx < (currentPageKey + numOfSibling + 1) && | ||
| idx > (currentPageKey - numOfSibling - 1)) | ||
| } | ||
| _keyExists(sceneKeys, key) { | ||
| return sceneKeys.find((sceneKey) => key === sceneKey) | ||
| } | ||
| _makeSceneKey(child, idx) { | ||
| return child.props.tabLabel + '_' + idx | ||
| } | ||
| renderScrollableContent() { | ||
| //in case of the collapsible scroll view the pull to refresh animation will be applied on the container | ||
| //on the other case the refresh animations will be applied here. | ||
| const isContainerScrollView = !!this.props.collapsableBar | ||
| const scenes = this._composeScenes() | ||
| return <Animated.ScrollView | ||
| refreshControl={!isContainerScrollView && this.props.pullToRefresh && typeof this.props.pullToRefresh === 'function' && | ||
| <RefreshControl style={this.props.refreshControlStyle || {}} | ||
| refreshing={this.state.refreshing} | ||
| onRefresh={this._onRefresh}/> || undefined} | ||
| showsVerticalScrollIndicator={this.props.showsVerticalScrollIndicator} | ||
| showsHorizontalScrollIndicator={this.props.showsHorizontalScrollIndicator} | ||
| horizontal | ||
| pagingEnabled | ||
| automaticallyAdjustContentInsets={false} | ||
| contentOffset={{x: this.props.initialPage * this.state.containerWidth}} | ||
| ref={scrollView => this.scrollView = scrollView} | ||
| onScroll={Animated.event([{nativeEvent: {contentOffset: {x: this.state.scrollX}}}], {useNativeDriver: true})} | ||
| onMomentumScrollBegin={this._onMomentumScrollBeginAndEnd} | ||
| onMomentumScrollEnd={this._onMomentumScrollBeginAndEnd} | ||
| scrollEventThrottle={16} | ||
| scrollsToTop={false} | ||
| scrollEnabled={!this.props.locked} | ||
| directionalLockEnabled | ||
| alwaysBounceVertical={false} | ||
| keyboardDismissMode="on-drag" | ||
| {...this.props.contentProps} | ||
| > | ||
| {scenes} | ||
| </Animated.ScrollView> | ||
| } | ||
| _composeScenes() { | ||
| return this._children().map((child, idx) => { | ||
| let key = this._makeSceneKey(child, idx) | ||
| let element | ||
| if (!!this.props.collapsableBar) { | ||
| element = this.state.currentPage === idx ? child : null | ||
| } else { | ||
| element = this._keyExists(this.state.sceneKeys, key) ? child : <View tabLabel={child.props.tabLabel}/> | ||
| } | ||
| return <SceneComponent | ||
| key={child.key} | ||
| shouldUpdated={!!this.props.collapsableBar || this._shouldRenderSceneKey(idx, this.state.currentPage)} | ||
| style={{width: this.state.containerWidth}} | ||
| > | ||
| {element} | ||
| </SceneComponent> | ||
| }) | ||
| } | ||
| _onMomentumScrollBeginAndEnd(e) { | ||
| const offsetX = e.nativeEvent.contentOffset.x | ||
| const page = Math.round(offsetX / this.state.containerWidth) | ||
| if (this.state.currentPage !== page) { | ||
| this._updateSelectedPage(page) | ||
| } | ||
| } | ||
| _updateSelectedPage(nextPage) { | ||
| let localNextPage = nextPage | ||
| if (typeof localNextPage === 'object') { | ||
| localNextPage = nextPage.nativeEvent.position | ||
| } | ||
| const currentPage = this.state.currentPage | ||
| this.updateSceneKeys({ | ||
| page: localNextPage, | ||
| callback: this._onChangeTab.bind(this, currentPage, localNextPage) | ||
| }) | ||
| } | ||
| _onChangeTab(prevPage, currentPage) { | ||
| this.props.onChangeTab({ | ||
| i: currentPage, | ||
| ref: this._children()[currentPage], | ||
| from: prevPage | ||
| }) | ||
| if (this.contentScrollDistance >= this.collapsableBarHeight) { | ||
| this.contentView.scrollTo({x: 0, y: this.collapsableBarHeight, animated: false}) | ||
| } | ||
| } | ||
| _handleLayout(e) { | ||
| const {width} = e.nativeEvent.layout | ||
| if (Math.round(width) !== Math.round(this.state.containerWidth)) { | ||
| this.setState({containerWidth: width}) | ||
| this.requestAnimationFrame(() => { | ||
| this.goToPage(this.state.currentPage) | ||
| }) | ||
| } | ||
| } | ||
| _children(children = this.props.children) { | ||
| return React.Children.map(children, (child) => child) | ||
| } | ||
| renderCollapsableBar() { | ||
| if (!this.props.collapsableBar) { | ||
| return null | ||
| } | ||
| return React.cloneElement(this.props.collapsableBar, { | ||
| onLayout: event => { | ||
| this.collapsableBarHeight = event.nativeEvent.layout.height | ||
| } | ||
| }) | ||
| } | ||
| render() { | ||
| let overlayTabs = (this.props.tabBarPosition === 'overlayTop' || this.props.tabBarPosition === 'overlayBottom') | ||
| let tabBarProps = { | ||
| goToPage: this.goToPage, | ||
| tabs: this._children().map((child) => child.props.tabLabel), | ||
| activeTab: this.state.currentPage, | ||
| scrollX: this.state.scrollX, | ||
| scrollValue: this.state.scrollValue, | ||
| containerWidth: this.state.containerWidth | ||
| } | ||
| if (this.props.tabBarBackgroundColor) { | ||
| tabBarProps.backgroundColor = this.props.tabBarBackgroundColor | ||
| } | ||
| if (this.props.tabBarActiveTextColor) { | ||
| tabBarProps.activeTextColor = this.props.tabBarActiveTextColor | ||
| } | ||
| if (this.props.tabBarInactiveTextColor) { | ||
| tabBarProps.inactiveTextColor = this.props.tabBarInactiveTextColor | ||
| } | ||
| if (this.props.tabBarTextStyle) { | ||
| tabBarProps.textStyle = this.props.tabBarTextStyle | ||
| } | ||
| if (this.props.tabBarUnderlineStyle) { | ||
| tabBarProps.underlineStyle = this.props.tabBarUnderlineStyle | ||
| } | ||
| if (overlayTabs) { | ||
| tabBarProps.style = { | ||
| position: 'absolute', | ||
| left: 0, | ||
| right: 0, | ||
| [this.props.tabBarPosition === 'overlayTop' ? 'top' : 'bottom']: 0 | ||
| } | ||
| } | ||
| const ContainerView = this.props.collapsableBar ? ScrollView : View | ||
| const isScrollView = this.props.collapsableBar ? true : false | ||
| return (<ContainerView | ||
| refreshControl={isScrollView && this.props.pullToRefresh && typeof this.props.pullToRefresh === 'function' && | ||
| <RefreshControl style={this.props.refreshControlStyle || {}} | ||
| refreshing={this.state.refreshing} | ||
| onRefresh={this._onRefresh}/> || undefined} | ||
| showsVerticalScrollIndicator={isScrollView && this.props.showsVerticalScrollIndicator} | ||
| showsHorizontalScrollIndicator={isScrollView && this.props.showsHorizontalScrollIndicator} | ||
| style={[styles.container, this.props.style]} | ||
| onLayout={this._handleLayout} //()=> | ||
| ref={contentView => this.contentView = contentView} | ||
| onMomentumScrollEnd={event => { | ||
| this.contentScrollDistance = event.nativeEvent.contentOffset.y | ||
| }} | ||
| stickyHeaderIndices={this.props.collapsableBar ? [1] : []}> | ||
| {this.renderCollapsableBar()} | ||
| {this.props.tabBarPosition === 'top' && this.renderTabBar(tabBarProps)} | ||
| {this.renderScrollableContent()} | ||
| {(this.props.tabBarPosition === 'bottom' || overlayTabs) && this.renderTabBar(tabBarProps)} | ||
| </ContainerView> | ||
| ) | ||
| } | ||
| } | ||
| ScrollableTabView.defaultProps = { | ||
| tabBarPosition: 'top', | ||
| initialPage: 0, | ||
| page: -1, | ||
| onChangeTab: () => { | ||
| }, | ||
| onScroll: () => { | ||
| }, | ||
| contentProps: {}, | ||
| scrollWithoutAnimation: false, | ||
| locked: false, | ||
| prerenderingSiblingsNumber: 0 | ||
| } | ||
| ScrollableTabView.propTypes = { | ||
| tabBarPosition: PropTypes.oneOf(['top', 'bottom', 'overlayTop', 'overlayBottom']), | ||
| initialPage: PropTypes.number, | ||
| page: PropTypes.number, | ||
| onChangeTab: PropTypes.func, | ||
| onScroll: PropTypes.func, | ||
| renderTabBar: PropTypes.any, | ||
| style: ViewPropTypes.style, | ||
| contentProps: PropTypes.object, | ||
| scrollWithoutAnimation: PropTypes.bool, | ||
| locked: PropTypes.bool, | ||
| prerenderingSiblingsNumber: PropTypes.number, | ||
| collapsableBar: PropTypes.node | ||
| } | ||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1 | ||
| }, | ||
| scrollableContentAndroid: { | ||
| flex: 1 | ||
| } | ||
| }) | ||
| module.exports = { | ||
| DefaultTabBar, | ||
| ScrollableTabBar, | ||
| ScrollableTabView | ||
| } |
| import React from 'react' | ||
| import ReactNative from 'react-native' | ||
| const {View, StyleSheet} = ReactNative | ||
| import StaticContainer from './StaticContainer' | ||
| export default SceneComponent = (Props) => { | ||
| const {shouldUpdated, ...props} = Props | ||
| return <View {...props}> | ||
| <StaticContainer shouldUpdate={shouldUpdated}> | ||
| {props.children} | ||
| </StaticContainer> | ||
| </View> | ||
| } |
| import React, {Component} from 'react' | ||
| import ReactNative from 'react-native' | ||
| import PropTypes from 'prop-types' | ||
| const { | ||
| ViewPropTypes, | ||
| Dimensions, | ||
| View, | ||
| Animated, | ||
| ScrollView, | ||
| StyleSheet, | ||
| InteractionManager, | ||
| Platform, | ||
| Text, | ||
| I18nManager | ||
| } = ReactNative | ||
| import Button from './Button' | ||
| const WINDOW_WIDTH = Dimensions.get('window').width | ||
| export default class ScrollableTabBar extends Component { | ||
| constructor(props) { | ||
| super(props) | ||
| this.state = this.getInitialState() | ||
| this.updateView = this.updateView.bind(this) | ||
| this.necessarilyMeasurementsCompleted = this.necessarilyMeasurementsCompleted.bind(this) | ||
| this.updateTabPanel = this.updateTabPanel.bind(this) | ||
| this.updateTabUnderline = this.updateTabUnderline.bind(this) | ||
| this.updateTabUnderline = this.updateTabUnderline.bind(this) | ||
| this.renderTab = this.renderTab.bind(this) | ||
| this.measureTab = this.measureTab.bind(this) | ||
| this.onTabContainerLayout = this.onTabContainerLayout.bind(this) | ||
| this.onContainerLayout = this.onContainerLayout.bind(this) | ||
| } | ||
| getInitialState() { | ||
| this._tabsMeasurements = [] | ||
| return { | ||
| _leftTabUnderline: new Animated.Value(0), | ||
| _widthTabUnderline: new Animated.Value(0), | ||
| _containerWidth: null | ||
| } | ||
| } | ||
| componentDidMount() { | ||
| this.props.scrollValue.addListener(this.updateView) | ||
| } | ||
| updateView(offset) { | ||
| const position = Math.floor(offset.value) | ||
| const pageOffset = offset.value % 1 | ||
| const tabCount = this.props.tabs.length | ||
| const lastTabPosition = tabCount - 1 | ||
| if (tabCount === 0 || offset.value < 0 || offset.value > lastTabPosition) { | ||
| return | ||
| } | ||
| if (this.necessarilyMeasurementsCompleted(position, position === lastTabPosition)) { | ||
| this.updateTabPanel(position, pageOffset) | ||
| this.updateTabUnderline(position, pageOffset, tabCount) | ||
| } | ||
| } | ||
| necessarilyMeasurementsCompleted(position, isLastTab) { | ||
| return this._tabsMeasurements[position] && | ||
| (isLastTab || this._tabsMeasurements[position + 1]) && | ||
| this._tabContainerMeasurements && | ||
| this._containerMeasurements | ||
| } | ||
| updateTabPanel(position, pageOffset) { | ||
| const containerWidth = this._containerMeasurements.width | ||
| const tabWidth = this._tabsMeasurements[position].width | ||
| const nextTabMeasurements = this._tabsMeasurements[position + 1] | ||
| const nextTabWidth = nextTabMeasurements && nextTabMeasurements.width || 0 | ||
| const tabOffset = this._tabsMeasurements[position].left | ||
| const absolutePageOffset = pageOffset * tabWidth | ||
| let newScrollX = tabOffset + absolutePageOffset | ||
| // center tab and smooth tab change (for when tabWidth changes a lot between two tabs) | ||
| newScrollX -= (containerWidth - (1 - pageOffset) * tabWidth - pageOffset * nextTabWidth) / 2 | ||
| newScrollX = newScrollX >= 0 ? newScrollX : 0 | ||
| if (Platform.OS === 'android') { | ||
| this._scrollView && this._scrollView.scrollTo({x: newScrollX, y: 0, animated: false}) | ||
| } else { | ||
| const rightBoundScroll = this._tabContainerMeasurements.width - (this._containerMeasurements.width) | ||
| newScrollX = newScrollX > rightBoundScroll ? rightBoundScroll : newScrollX | ||
| this._scrollView && this._scrollView.scrollTo({x: newScrollX, y: 0, animated: false}) | ||
| } | ||
| } | ||
| updateTabUnderline(position, pageOffset, tabCount) { | ||
| const lineLeft = this._tabsMeasurements[position].left | ||
| const lineRight = this._tabsMeasurements[position].right | ||
| if (position < tabCount - 1) { | ||
| const nextTabLeft = this._tabsMeasurements[position + 1].left | ||
| const nextTabRight = this._tabsMeasurements[position + 1].right | ||
| const newLineLeft = (pageOffset * nextTabLeft + (1 - pageOffset) * lineLeft) | ||
| const newLineRight = (pageOffset * nextTabRight + (1 - pageOffset) * lineRight) | ||
| this.state._leftTabUnderline.setValue(newLineLeft) | ||
| this.state._widthTabUnderline.setValue(newLineRight - newLineLeft) | ||
| } else { | ||
| this.state._leftTabUnderline.setValue(lineLeft) | ||
| this.state._widthTabUnderline.setValue(lineRight - lineLeft) | ||
| } | ||
| } | ||
| renderTab(name, page, isTabActive, onPressHandler, onLayoutHandler) { | ||
| const {activeTextColor, inactiveTextColor, textStyle} = this.props | ||
| const textColor = isTabActive ? activeTextColor : inactiveTextColor | ||
| const fontWeight = isTabActive ? 'bold' : 'normal' | ||
| return <Button | ||
| key={`${name}_${page}`} | ||
| accessible={true} | ||
| accessibilityLabel={name} | ||
| accessibilityTraits='button' | ||
| onPress={() => onPressHandler(page)} | ||
| onLayout={onLayoutHandler} | ||
| > | ||
| <View style={[styles.tab, this.props.tabStyle]}> | ||
| <Text style={[{color: textColor, fontWeight}, textStyle]}> | ||
| {name} | ||
| </Text> | ||
| </View> | ||
| </Button> | ||
| } | ||
| measureTab(page, event) { | ||
| const {x, width, height} = event.nativeEvent.layout | ||
| this._tabsMeasurements[page] = {left: x, right: x + width, width, height} | ||
| this.updateView({value: this.props.scrollValue._value}) | ||
| } | ||
| render() { | ||
| const tabUnderlineStyle = { | ||
| position: 'absolute', | ||
| height: 4, | ||
| backgroundColor: 'navy', | ||
| bottom: 0 | ||
| } | ||
| const key = I18nManager.isRTL ? 'right' : 'left' | ||
| const dynamicTabUnderline = { | ||
| [`${key}`]: this.state._leftTabUnderline, | ||
| width: this.state._widthTabUnderline | ||
| } | ||
| return <View | ||
| style={[styles.container, {backgroundColor: this.props.backgroundColor}, this.props.style]} | ||
| onLayout={this.onContainerLayout} | ||
| > | ||
| <ScrollView | ||
| automaticallyAdjustContentInsets={false} | ||
| ref={(scrollView) => { | ||
| this._scrollView = scrollView | ||
| }} | ||
| horizontal={true} | ||
| showsHorizontalScrollIndicator={false} | ||
| showsVerticalScrollIndicator={false} | ||
| directionalLockEnabled={true} | ||
| onScroll={this.props.onScroll} | ||
| bounces={false} | ||
| scrollsToTop={false} | ||
| > | ||
| <View | ||
| style={[styles.tabs, {width: this.state._containerWidth}, this.props.tabsContainerStyle]} | ||
| ref={'tabContainer'} | ||
| onLayout={this.onTabContainerLayout} | ||
| > | ||
| {this.props.tabs.map((name, page) => { | ||
| const isTabActive = this.props.activeTab === page | ||
| const renderTab = this.props.renderTab || this.renderTab | ||
| return renderTab(name, page, isTabActive, this.props.goToPage, this.measureTab.bind(this, page)) | ||
| })} | ||
| <Animated.View style={[tabUnderlineStyle, dynamicTabUnderline, this.props.underlineStyle]}/> | ||
| </View> | ||
| </ScrollView> | ||
| </View> | ||
| } | ||
| componentWillReceiveProps(nextProps) { | ||
| // If the tabs change, force the width of the tabs container to be recalculated | ||
| if (JSON.stringify(this.props.tabs) !== JSON.stringify(nextProps.tabs) && this.state._containerWidth) { | ||
| this.setState({_containerWidth: null}) | ||
| } | ||
| } | ||
| onTabContainerLayout(e) { | ||
| this._tabContainerMeasurements = e.nativeEvent.layout | ||
| let width = this._tabContainerMeasurements.width | ||
| if (width < WINDOW_WIDTH) { | ||
| width = WINDOW_WIDTH | ||
| } | ||
| this.setState({_containerWidth: width}) | ||
| this.updateView({value: this.props.scrollValue._value}) | ||
| } | ||
| onContainerLayout(e) { | ||
| this._containerMeasurements = e.nativeEvent.layout | ||
| this.updateView({value: this.props.scrollValue._value}) | ||
| } | ||
| } | ||
| ScrollableTabBar.defaultProps = { | ||
| scrollOffset: 52, | ||
| activeTextColor: 'navy', | ||
| inactiveTextColor: 'black', | ||
| backgroundColor: null, | ||
| style: {}, | ||
| tabStyle: {}, | ||
| tabsContainerStyle: {}, | ||
| underlineStyle: {} | ||
| } | ||
| ScrollableTabBar.propTypes = { | ||
| goToPage: PropTypes.func, | ||
| activeTab: PropTypes.number, | ||
| tabs: PropTypes.array, | ||
| backgroundColor: PropTypes.string, | ||
| activeTextColor: PropTypes.string, | ||
| inactiveTextColor: PropTypes.string, | ||
| scrollOffset: PropTypes.number, | ||
| style: ViewPropTypes.style, | ||
| tabStyle: ViewPropTypes.style, | ||
| tabsContainerStyle: ViewPropTypes.style, | ||
| textStyle: Text.propTypes.style, | ||
| renderTab: PropTypes.func, | ||
| underlineStyle: ViewPropTypes.style, | ||
| onScroll: PropTypes.func | ||
| } | ||
| const styles = StyleSheet.create({ | ||
| tab: { | ||
| height: 49, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| paddingLeft: 20, | ||
| paddingRight: 20 | ||
| }, | ||
| container: { | ||
| height: 50, | ||
| borderWidth: 1, | ||
| borderTopWidth: 0, | ||
| borderLeftWidth: 0, | ||
| borderRightWidth: 0, | ||
| borderColor: '#ccc' | ||
| }, | ||
| tabs: { | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-around' | ||
| } | ||
| }) |
| import React, {Component} from 'react' | ||
| export default class StaticContainer extends Component { | ||
| shouldComponentUpdate(nextProps: Object): boolean { | ||
| return !!nextProps.shouldUpdate | ||
| } | ||
| render(): ?ReactElement { | ||
| const child = this.props.children | ||
| if (child === null || child === false) { | ||
| return null | ||
| } | ||
| return React.Children.only(child) | ||
| } | ||
| } | ||
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
35605
0.51%0
-100%158
0.64%734
-0.94%1
Infinity%- Removed
- Removed